��������������������������������������������� ����������������������������������������������������������������� ���������������������������������������������������������������� ��������������������������������������������������������������� �������������������������������������������������� ���������������������������������������������������������������������������������� �������������������������������������������������������������������������������� ����������������������������������������������������������������������������������� ������������������������������������������������������������������������������������� ������� ������������ ������ �� ������������ ���������� ��������� ������������� ����������� ���������� ����������� ����� ����������� ������ ������������ ����� ������������� ������� ��� ���������� �������� ��� ��������� ���������� ��������� ��������� �������� ���������������������������������������������������������������������������������� �������������������������������������������������������������������������������� ���������������������������������������������������������������������������� ������� ��� ������������ ������������ ������ ������� �������������� ������� ��������� ������������������������������������������������������������������������������������ �����������������������������������������������������
����������������������������������
Prodáno 14.10.2014 na www.Kosmas.cz zákazníkovi
[email protected]
��������������������������������������������� ����������������������������������������������������������������� ���������������������������������������������������������������� ��������������������������������������������������������������� �������������������������������������������������� ���������������������������������������������������������������������������������� �������������������������������������������������������������������������������� ����������������������������������������������������������������������������������� ������������������������������������������������������������������������������������� ������� ������������ ������ �� ������������ ���������� ��������� ������������� ����������� ���������� ����������� ����� ����������� ������ ������������ ����� ������������� ������� ��� ���������� �������� ��� ��������� ���������� ��������� ��������� �������� ���������������������������������������������������������������������������������� �������������������������������������������������������������������������������� ���������������������������������������������������������������������������� ������� ��� ������������ ������������ ������ ������� �������������� ������� ��������� ������������������������������������������������������������������������������������ �����������������������������������������������������
����������������������������������
Prodáno 14.10.2014 na www.Kosmas.cz zákazníkovi
[email protected], číslo objednávky 591522, UID: 7e15c933-e4b8-4e2a-94a6-c3fd2dffdec9
��������������������������������������������� ����������������������������������������������������������������� ���������������������������������������������������������������� ��������������������������������������������������������������� �������������������������������������������������� ���������������������������������������������������������������������������������� �������������������������������������������������������������������������������� ����������������������������������������������������������������������������������� ������������������������������������������������������������������������������������� ������� ������������ ������ �� ������������ ���������� ��������� ������������� ����������� ���������� ����������� ����� ����������� ������ ������������ ����� ������������� ������� ��� ���������� �������� ��� ��������� ���������� ��������� ��������� �������� ���������������������������������������������������������������������������������� �������������������������������������������������������������������������������� ���������������������������������������������������������������������������� ������� ��� ������������ ������������ ������ ������� �������������� ������� ��������� ������������������������������������������������������������������������������������ �����������������������������������������������������
����������������������������������
Prodáno 14.10.2014 na www.Kosmas.cz zákazníkovi
[email protected], číslo objednávky 591522, UID: 7e15c933-e4b8-4e2a-94a6-c3fd2dffdec9
DIRECTX
5
Obsah Úvod ...........................................................................................7 Předpoklady ke studiu této knihy .............................................................................. 8 Koncepce knihy ........................................................................................................ 8 Zdrojové kódy knihy .................................................................................................. 9 Co budeme potřebovat ............................................................................................. 9 Poděkování a podpora ............................................................................................ 10
1. Bližší seznámení s DirectX ....................................................11 1.1 Součásti DirectX ...........................................................................12 1.2 Pohled do historie DirectX .............................................................13
2. Základní aplikace .................................................................15 2.1 Nastavení DirectX pro naše aplikace ..............................................15 2.2 Vytvoření základní Win32 aplikace ...............................................18 2.3 Zjištění verze DirectX ...................................................................25
3. DirectDraw ..........................................................................29 3.1 Jednoduchá aplikace s DirectDraw .................................................31 3.2 Triple buffering ............................................................................43 3.3 DirectDraw v okně .......................................................................48 3.4 Off-screen surfaces ......................................................................52 3.5 Barevná paleta .............................................................................58 3.6 Barevný klíč .................................................................................62
4. Direct3D ..............................................................................73 4.1 Základní pojmy a transformace .....................................................76 4.1.1 Souřadnicový systém ..................................................................................... 77 4.1.2 Vertexy, hrany a plochy .................................................................................. 77 4.1.3 Transformace a matice .................................................................................. 78
4.2 Jednoduchá Direct3D aplikace v okně .............................................80
Obsah
Prodáno 14.10.2014 na www.Kosmas.cz zákazníkovi
[email protected], číslo objednávky 591522, UID: 7e15c933-e4b8-4e2a-94a6-c3fd2dffdec9
6
DIRECTX
4.3 Direct3D v celoobrazovkovém režimu ............................................90 4.4 Surfaces v Direct3D ......................................................................94 4.5 Vykreslování jednoduchých objektů .............................................104 4.6 Práce s index bufferem ...............................................................113 4.7 Transformace .............................................................................118 4.8 Práce se složitějšími objekty a formát .x......................................124 4.9 Materiály ...................................................................................131 4.10 Světla ........................................................................................134 4.11 Textury .....................................................................................140 4.12 Další využití textur a transformace objektů .................................147 4.13 Práce s texty .............................................................................154 4.14 Práce s kamerou a okolí scény .....................................................163
5. DirectInput.........................................................................179 5.1 Práce s klávesnicí .......................................................................180 5.2 Práce s myší...............................................................................189
6. DirectSound a DirectMusic...................................................195 6.1 DirectSound................................................................................197 6.2 DirectMusic ................................................................................201
7. DirectPlay ..........................................................................207 7.1 Komunikace peer-to-peer ............................................................209 7.2 Komunikace klient/server ...........................................................215
8. Co dál? ..............................................................................217 8.1 Použité zdroje ............................................................................218
Rejstřík ...................................................................................221
Obsah
Prodáno 14.10.2014 na www.Kosmas.cz zákazníkovi
[email protected], číslo objednávky 591522, UID: 7e15c933-e4b8-4e2a-94a6-c3fd2dffdec9
7
Úvod
DIRECTX
Úvod Pokud jste otevřeli tuto knihu, s největší pravděpodobností alespoň tušíte, co to DirectX vlastně je. Jde o kolekci pomocných softwarových nástrojů tvořících programátorské rozhraní, tzv. API (Application Programming Interface) pro tvorbu multimediálních a herních aplikací. Vyvíjí ho firma Microsoft, proto se s ním můžete setkat jednak v operačním systému Windows a dále na platformách této firmy, jimiž jsou herní konzoly XBox (těmi se v této knize zabývat nebudeme). V této knize se zaměříme na tvorbu aplikací ve Windows. Snad úplně každý počítačový uživatel si zkusil zahrát nějakou počítačovou hru, a právě u počítačových her se s DirectX můžeme setkat velice často. Důvody jsou jednoduché. DirectX poskytuje velice kvalitní nástroje pro práci s 2D i 3D grafikou, práci se vstupními zařízeními, se zvuky a hudbou, ale i pro komunikaci mezi počítači. A právě tyto věci jsou u multimediálních a herních aplikací stěžejní. Pokud tedy chcete nahlédnout pod pokličku tvorby takových programů, je tato kniha určena právě pro vás. Provede vás základy tvorby DirectX aplikací pro verzi DirectX 9.0c. Po jejím pečlivém prostudování byste měli být schopni ve svých programech používat jednotlivé komponenty DirectX. Nicméně vás zcela jistě nenaučí všemu. Celá problematika má totiž mnohem větší rozsah, který tato kniha nestačí pokrýt. Proto chce-
Úvod
Prodáno 14.10.2014 na www.Kosmas.cz zákazníkovi
[email protected], číslo objednávky 591522, UID: 7e15c933-e4b8-4e2a-94a6-c3fd2dffdec9
8
DIRECTX
te-li se této oblasti věnovat opravdu důkladně, doporučuji prostudovat publikace, jejichž seznam se nachází na konci této knihy, a aktuálně sledovat internetové servery, věnující se této problematice.
Předpoklady ke studiu této knihy Pro úspěšné studium této knihy budu předpokládat, že znáte programovací jazyk C++. Veškeré příklady a programové ukázky jsou právě v tomto jazyce. Nemůžeme zde vysvětlovat obecné programovací principy a základní pojmy jako proměnná, pole, ukazatel, objekt, zapouzdření, přístupová práva ve třídách atd. Pokud s programováním začínáte nebo částečně ovládáte jazyk C, doporučuji nejprve prostudovat nějakou odbornou publikaci, například [2]. Výhodou, ale ne nutností je také alespoň základní znalost Win32 API. Při programování jednodušších DirectX aplikací toho o Win32 API mnoho vědět nepotřebujete. To, co bude nutné z Win32 API vědět, se dočtete v této knize. Nicméně Win32API je velice rozsáhlé a nabízí spoustu užitečných funkcí, které můžete využít i později. Proto je vhodné mít o jeho možnostech alespoň přehled. Něco z Win32API využijeme i v ukázkových programech této knihy. Kompletní popis Win32API můžete získat studiem knih [4] nebo [7]. Kromě tohoto jsou také vhodné znalosti určitých oblastí matematiky a počítačové grafiky (geometrie, trigonometrie, lineární algebra, rasterizace, grafické formáty, základy promítání apod.), které vám usnadní práci s Direct3D. Samozřejmě, že mnoho informací a ukázkových programů jazyka C++ a Win32 API naleznete i na internetu. Některé internetové odkazy naleznete i na konci této knihy, nebo můžete použít svůj oblíbený vyhledávač. Existuje mnoho webových serverů, které se tímto zabývají. Také tyto zdroje bývají doprovázeny četnými ukázkami programových kódů, jejichž prostudováním a praktickým vyzkoušením se toho naučíte nejvíce.
Koncepce knihy Celková koncepce knihy se může na první pohled zdát ne zrovna vhodná. Ve většině kapitol jsou jednotlivá rozhraní, jejich objekty a metody vysvětleny přímo na příkladech. Důvodem chybějícího teoretického popisu ještě před příkladem je skutečnost, že nejde o příliš obsáhlá témata a přímo z programového kódu by měl být takový popis mnohem názornější. Naopak v místech, kdy by teorie bylo příliš mnoho, což by narušovalo plynulost popisu programu, je tento teoretický popis na začátku příslušné kapitoly uveden. V knize je zejména kladen důraz na popis rozhraní, objektů, funkcí a datových typů, které patří do DirectX. Až na poslední dvě kapitoly (věnované DirectSound, DirectMusic a DirectPlay) je vše demonstrováno na funkčních programových ukázkách. Nebojte s nimi experimentovat a libovolně je upravovat. A nenechte se odradit chybami, které vám při tom možná bude hlásit kompilátor. I o tom je totiž práce programátora. Zkušený programátor ví, že nejde jen o napsání nějakého programového kódu, ale i o jeho efektivitu, rychlost, paměťové nároky atd. A nedílnou součástí programů je i vyhledávání a odstraňování chyb v programech, tzv. ladění. Ostatně jedno přísloví říká, že chybami se člověk učí. Pro přehlednost textu v knize jsou příkazové nabídky či názvy souborů psány tučně a anglické výrazy jsou psány kurzívou. Zdrojové programy a programové příkazy jsou pak odlišným fontem. Občas je možné v textu objevit slova, která jsou v určitých případech odlišným fontem psána a jindy ne. Příkladem je DirectDraw. Zde nejde o chybu, ale opět o odlišení. DirectDraw je myšleno jako komponenta DirectX, zatímco DirectDraw představuje objekt, tedy část programu.
Úvod
Prodáno 14.10.2014 na www.Kosmas.cz zákazníkovi
[email protected], číslo objednávky 591522, UID: 7e15c933-e4b8-4e2a-94a6-c3fd2dffdec9
V této knize nejsou některé anglické výrazy překládány do češtiny. V některých českých publikacích tohoto typu naleznete pro výrazy „surface“, „front buffer“ a „back buffer“ překlady jako „povrch“, „přední povrch“ a „zadní povrch“. Osobně se však raději držím anglické terminologie, aniž by však tyto výrazy byly psány kurzívou. Je to výhoda i pro čtenáře, kteří budou studovat anglicky psané publikace tohoto typu.
9
Úvod
DIRECTX
Zdrojové kódy knihy Programové ukázky, které naleznete v této knize, nemusíte opisovat. Stačí navštívit domovskou stránku nakladatelství Grada (www.grada.cz) a tam v sekci Soubory a příklady ke stažení tuto knihu vyhledat. Pod přískušným odkazem jsou tyto zdrojové kódy k dispozici. Po dekompresi staženého souboru se vytvoří složky, odpovídající číslům kapitol v této knize. V těchto složkách se nachází programy ve spustitelné formě a dále ve formě zdrojových kódů. Veškeré zdrojové kódy jsou doplněny četnými komentáři, které by měly usnadnit jejich pochopení.
Co budeme potřebovat Než začneme programovat v DirectX, musíme mít dvě věci. První z nich je vývojové prostředí s kompilátorem. Jak jistě víte, kompilátorů existuje celá řada. U profesionálních vývojářů patří mezi nejčastěji používané Microsoft Visual Studio (nás konkrétně zajímá Microsoft Visual C++). V něm byly vytvořeny všechny programy, které v této knize naleznete. Doporučuji ho i vám, protože u jiných vývojových nástrojů mohou nastat problémy s nastavením a kompilací. Jak jistě víte, Microsoft Visual C++ je komerční produkt. Nevlastníte-li toto vývojové prostředí, nemusíte zoufat. Existují i zkušební verze určené pro začátečníky a amatéry, kteří se chtějí naučit programovat. Zpočátku tedy nemusíte tímto směrem vynakládat žádné investice a teprve později, pokud budete chtít využívat všech předností, jimiž toto vývojové prostředí disponuje, si můžete některou z komerčních verzí Visual C++ zakoupit. Nejnovější verze, která je k dispozici zdarma, se jmenuje Microsoft Visual C++ 2005 Express Edition a stáhnout si ji můžete z internetových stránek Microsoftu [21]. Po instalaci a následném spuštění ve výběru nových projektů zjistíte, že je možné vytvořit pouze Win32 konzolové aplikace. Pro aplikace využívající DirectX potřebujeme čisté Win32 aplikace. Tuto komplikaci dokáže vyřešit programový balík Platform SDK, který si opět můžete bezplatně stáhnout z internetu. Po jeho instalaci musíme provést ještě určitá nastavení v našem vývojovém prostředí. Nastavení je poněkud více, ale nejsou nijak složitá. Pokud tedy chcete používat bezplatnou zkušební verzi Visual C++ a vytvářet v něm Win32 aplikace, stačí se držet postupu, který je popsán na internetových stránkách Microsoftu [23]. Kromě bezplatné verze Express je tu samozřejmě možnost zakoupení plné verze Microsoft Visual Studia (C++). Pokud s programováním začínáte a ještě nevíte, zda se programování stane vaším koníčkem nebo se jím budete živit, bude vám produkt nejspíše připadat velice drahý. V opačném případě se vám taková investice jistě vyplatí. Pokud však ještě nejste rozhodnuti nebo patříte spíše do první skupiny uživatelů a nechcete zpočátku příliš investovat, nabízí se ještě možnost pořídit si verzi starší. Předchozí verze nese název Microsoft Visual C++ 2003 .NET a té ještě předcházela verze Microsoft Visual C++ 6.0. Tyto starší verze se příležitostně dají zakoupit levněji. Příklady, které jsou uvedeny v této knize, jsou vytvořeny v Microsoft Visual C++ 6.0. I samotný popis tvorby a nastavení projektů se týká této verze. Nicméně prostředí obou
Úvod
Prodáno 14.10.2014 na www.Kosmas.cz zákazníkovi
[email protected], číslo objednávky 591522, UID: 7e15c933-e4b8-4e2a-94a6-c3fd2dffdec9
10
DIRECTX
novějších verzí je do značné míry koncipováno podobně jako ve verzi 6.0. Proto to, co je popsáno v následujících kapitolách, platí i pro novější verze. Samozřejmostí je, že v těchto novějších verzích můžete velice jednoduše zkompilovat všechny programy, které jsou uvedeny v této knize. Pokud nechcete používat Microsoft Visual C++, nabízejí se i alternativy. Ať už se jedná o libovolný komerční nebo nekomerční vývojový nástroj, vyplatí se pečlivě prostudovat příslušnou dokumentaci a případě zjistit osobní zkušenosti jiných počítačových uživatelů a vyhnout se tak zbytečné investici. Protože je DirectX produkt firmy Microsoft, je nejlépe podporován vývojovými nástroji této firmy. U jiných vývojových nástrojů může mít použití DirectX jistá omezení, například nemusí správně fungovat všechny jeho součásti nebo může existovat podpora pouze zastaralých verzí. Druhou věcí, kterou budeme pro naše programy potřebovat, je DirectX SDK (Software Development Kit). Jde o softwarový balík, který si můžete zdarma stáhnout ze stránek Microsoftu. [14] To nejdůležitější, co z něj potřebujeme, jsou hlavičkové soubory a knihovny, které podle implementovaných částí DirectX vkládáme a přilinkováváme do programových projektů (o způsobu implementace se můžete dočíst v druhé kapitole). Dále zde také můžeme nalézt i podrobnou dokumentaci a spoustu ukázkových programů i se zdrojovými kódy, které nám při studiu DirectX také velice pomohou. V současné době je k dispozici SDK pro DirectX verzi 9.0c. Tato verze již existuje několik let, ovšem každé dva měsíce pravidelně vycházejí aktualizace, které přinášejí jisté změny a vylepšení. To, co přibylo nebo bylo upraveno, je vždy popsáno v souboru directx9_c.chm, který naleznete ve složce Documentation v instalaci DirectX SDK. Samozřejmě je nejvýhodnější používat vždy nejnovější SDK. Pokud však při kompilaci chcete používat starší vývojové prostředí Microsoft Visual C++ 6.0, narazíte u nových DirectX SDK na problém, neboť většina programů nejspíše nepůjde zkompilovat. Důvod je ten, že Visual C++ 6.0 je starší vývojové prostředí a není již více než dva roky oficiálně podporováno. Proto používáte-li tento kompilátor, je potřeba si z internetu stáhnout starší verzi DirectX SDK (o podpoře kompilátorů se můžete dočíst v dokumentaci ke každé verzi SDK). Pokud ale s DirectX začínáte a budete programy vytvářet podle této knihy, nemusíte se ničeho obávat. Zde uvedené programy jsou koncipovány tak, aby šly kompilovat ve Visual C++ 6.0 i starších DirectX 9.0c SDK. Na internetových stránkách Microsoftu [14] si můžete stáhnout jak nové, tak i starší SDK.
Poděkování a podpora Na tomto místě bych chtěl poděkovat všem lidem, kteří mi umožnili tuto knihu napsat. Jde především o množství nejmenovaných programátorů, kteří se prostřednictvím internetu nebáli zveřejnit svoje osobní zkušenosti s DirectX na nejrůznějších příkladech a programech. Výčet často navštěvovaných internetových stránek, které se programováním v DirectX zabývají, najdete na konci této knihy. Ale poděkování patří i všem lidem v mém blízkém okolí, jejichž větší či menší pomoc, trpělivost a tolerance mi umožnily tuto knihu napsat. Autor
Tato publikace vznikla za podpory výzkumného záměru MSM 7088352102.
Úvod
Prodáno 14.10.2014 na www.Kosmas.cz zákazníkovi
[email protected], číslo objednávky 591522, UID: 7e15c933-e4b8-4e2a-94a6-c3fd2dffdec9
1.
11
1. Bližší seznámení s DirectX
DIRECTX
Bližší seznámení s DirectX V úvodu jsme se krátce zmínili o tom, že DirectX je tzv. API rozhraní, které bylo navrženo pro snazší tvorbu multimediálních a herních aplikací. Toto API rozhraní obsahuje několik součástí, které jsou u podobných aplikací často používány. Jde především o nástroje pro práci s 2D/3D grafikou, zvuky (hudbou), síťovou komunikaci mezi počítači, vstupními zařízeními a přehrávání. Setkat se s DirectX nemusíme jen u počítačových her nebo multimedií, ale stále častěji se také uplatňuje v aplikacích, které jsou vytvářeny pro praktické využití v průmyslových odvětvích (například strojírenství). Důvodem je zejména Direct3D, které je hardwarově podporováno výrobci grafických karet a dokáže tak rychle poskytnout vysoce kvalitní 3D grafiku [13]. Z programátorského pohledu je DirectX založeno na tzv. COM (Component Object Model). COM představuje softwarovou architekturu definující vzájemné vztahy mezi jednotlivými komponentami. S rostoucími možnostmi počítačů roste i složitost programů. Proto se programy mohou skládat z komponent (komponenta je část programu, která má jisté specifikace). Jednotlivé komponenty společně komunikují prostřednictvím společného protokolu. COM tak poskytuje sjednocený, rozšířitelný, objektově orientovaný, komuni-
1. Bližší seznámení s DirectX
Prodáno 14.10.2014 na www.Kosmas.cz zákazníkovi
[email protected], číslo objednávky 591522, UID: 7e15c933-e4b8-4e2a-94a6-c3fd2dffdec9
12
DIRECTX
kační protokol. Jednoznačnou výhodou COM je tak přesná specifikace rozhraní, která zjednodušuje vývojářům tvorbu nových aplikací [1]. Pokud bychom měli uvést nějakou nevýhodu DirectX, je jí platformní závislost. DirectX je spojen pouze s Microsoft Windows, proto vyvíjené aplikace, které ho využívají, nelze zprovoznit pod jiným operačním systémem. Uživatelé, kteří chtějí používat DirectX programy navíc musí mít navíc nainstalovaný tzv. DirectX end-user runtime, který bývá součástí médií s takovými aplikacemi, nebo je možné ho zdarma stáhnout z internetových stránek Microsoftu [14]. Samozřejmě je přitom žádoucí, aby byla nainstalovaná nejnovější verze, kterou je ve Windows XP v současné době DirectX 9.0c.
1.1 Součásti DirectX V této podkapitole si blíže představíme jednotlivá rozhraní, která jsou součástí DirectX 9.0c. Více se o nich samozřejmě dozvíme v dalších kapitolách této knihy, kde bude popsána tvorba programových aplikací využívajících tato rozhraní. Rozhraní je několik a vývojáři ocení jednu velice příznivou skutečnost – jejich programová implementace je velice podobná. Proto se nemusíte obávat, že se musíte složitě učit každou součást DirectX zvlášť. Pokud se tedy naučíte používat jedno z těchto rozhraní, nebude pro vás složité zvládnout rozhraní jiné.
DirectX Graphics – zahrnuje v sobě DirectDraw a Direct3D.
DirectDraw – používá se při práci s rastrovou 2D grafikou, přičemž důraz je kladen na maximální výkonnost. Podporuje hardwarovou akceleraci, tedy čím více funkcí je podporováno grafickou kartou, tím rychlejší jsou i programy, které tyto funkce využívají. Pokud taková podpora chybí, tyto funkce se emulují softwarově, ovšem právě za cenu ztráty výkonu. DirectDraw umožňuje vytvářet aplikace, jež běží v okně i v celoobrazovkovém režimu. V současné době se již DirectDraw nevyvíjí, poslední aktualizace tohoto API byly provedeny v DirectX verzi 7 a od verze DirectX 8 je DirectDraw spojeno s Direct3D. Nicméně ho můžete samozřejmě používat i v DirectX 9. Direct3D – jak z názvu vyplývá, toto rozhraní je určeno pro práci s 3D grafikou (obsahuje i několik funkcí pro práci s 2D grafikou). Podobně jako u DirectDraw je i zde kladen důraz na maximální rychlost a výkonnost. Proto také staví na hardwarové akceleraci a nejvyšších výkonů dosáhneme, pokud máme grafickou kartu, která z tohoto rozhraní hardwarově podporuje co nejvíce funkcí. Mezi možnosti Direct3D patří například kreslení a prohlížení objektů, definování formátu pixelů, používání světelných zdrojů ve scéně, vylepšování obrazu (např. antialiasing, tedy vyhlazení ostrých hran), mapování textur nebo animování. To vše je samozřejmě možné provádět v aplikacích běžících v okně nebo v celoobrazovkovém režimu.
DirectX Input – jeho součástí je DirectInput, což je rozhraní, které poskytuje služby nejrůznějších vstupních zařízení, jako je klávesnice, myš, joystick nebo gamepad. Součástí je i podpora tzv. action mapping. To znamená, že u vstupních zařízeních s programovatelnými tlačítky můžeme těmto tlačítkům přiřadit námi určené funkce. DirectX Audio – součástí tohoto API je DirectSound. Jak již z názvu vyplývá, jde o rozhraní umožňující komunikaci se zvukovými kartami, umožňuje tedy vytvářet
1. Bližší seznámení s DirectX
Prodáno 14.10.2014 na www.Kosmas.cz zákazníkovi
[email protected], číslo objednávky 591522, UID: 7e15c933-e4b8-4e2a-94a6-c3fd2dffdec9
DIRECTX
13
DirectMusic – seskupení objektů, které umožňuje podobně jako DirectSound přehrávání zvuků a hudby, ovšem na vyšší úrovni než DirectSound. DirectMusic má více možností, například přehrávání více zvukových formátů (především MIDI), kompletní systém pro implementaci dynamických zvukových stop, které se mohou měnit na základě určitých událostí, dále downloadable sounds (DLS – standard pro syntetizaci zvuků z digitálních vzorků uložených v programech, který zajistí stejný zvukový výstup na všech počítačích) a 3D polohové efekty hudebních a zvukových zdrojů.
1. Bližší seznámení s DirectX
aplikace se zvuky a hudbou. Toto rozhraní je sdílené. Ke zvukové kartě tak může přistupovat několika aplikací zároveň. Samozřejmostí jsou základní i rozšiřující funkce, jimiž jsou například míchání (mixování) těchto zvuků, softwarové nastavování hlasitostí či urychlování/zpomalování přehrávání skladeb. Součástí je i práce s 3D zvuky (DirectSound3D).
DirectPlay – jde o knihovnu objektů, určenou pro komunikaci s jinými počítači, které jsou mezi sebou vzájemně síťově propojeny. Využití tedy nalezne především v multiplayerových počítačových hrách nebo chatovacích aplikacích. Jednotlivé počítače mohou mezi sebou komunikovat přímo (peer-to-peer), nebo pomocí centrálního serveru (klient/server komunikace).
1.2 Pohled do historie DirectX Vznik DirectX je spojen s operačním systémem Microsoft Windows 95. Oficiálně byl tento operační systém vydán v roce 1994 a pochopitelnou snahou Microsoftu bylo ho co nejvíce rozšířit mezi uživatele. To znamená, že muselo být možné dobře a efektivně tvořit co nejširší spektrum kvalitních aplikací. Oblast tvorby počítačových her pod Windows 95 byla problematická, protože funkce, které Windows poskytovala, byly pomalé a jejich možnosti omezené. Důvodem byl především chráněný paměťový režim, který blokoval přímý přístup k zařízením typu grafická či zvuková karta. Naproti tomu ve starším operačním systému (DOS) tento přímý přístup možný byl, proto i po uvedení Windows 95 byly počítačové hry stále vyvíjeny pro DOS [13]. Microsoft pochopitelně hledal cestu, jak zmíněná omezení Windows odstranit. Z těchto důvodů vzniklo DirectX. První verze byla uvolněna v září 1995 a nesla ještě původní název „Windows Games SDK“. Vývojáři navrhli základní grafickou technologii, která byla již od počátku tvořena tak, aby poskytovala co největší výkonnost. S postupem času, kdy narůstaly možnosti grafických karet a rychlost počítačů, bylo nutné provádět i aktualizace a rozšiřovat schopnosti DirectX. Jen tak mohlo držet krok s dobou a stále více se prosazovat v počítačových hrách, a posléze i mimo ně (v oblasti počítačové grafiky bylo a je nejvážnějším „konkurentem“ OpenGL). Proto vznikaly stále častěji nové verze. Přehled nejdůležitějších verzí s datem vydání a poznámkami obsahuje tabulka 1.1. Tabulka1.1: Přehled nejdůležitějších verzí DirectX. Zdroj: [13]
Verze
Datum vydání
DirectX 1.0
30. 9. 1995
Poznámky
DirectX 2.0/2.0a
5. 6. 1996
Určeno pro Windows 95 OSR2 a Windows NT 4.0.
DirectX 3.0/3.0a
15. 9. 1996
Poslední verze, která podporovala Windows NT 4.0.
1.2 Pohled do historie DirectX
Prodáno 14.10.2014 na www.Kosmas.cz zákazníkovi
[email protected], číslo objednávky 591522, UID: 7e15c933-e4b8-4e2a-94a6-c3fd2dffdec9
14
DIRECTX ?
Verze
Datum vydání
Poznámky
DirectX 5.0
16. 7. 1997
Jako beta je dostupné i pro Windows NT 5.0.
DirectX 5.1
1. 12. 1997
DirectX 5.2
5. 5. 1998
DirectX 6.0
7. 8. 1998
DirectX 6.1
3. 2. 1999
Určeno i pro Windows 98 SE.
DirectX 7.0
22. 9. 1999
Verze pro Windows 2000.
DirectX 7.1
26. 9. 1999
Určeno i pro Windows 98 ME.
DirectX 8.0
30. 9. 2000
DirectX 8.0a
7. 11. 2000
Poslední verze, která šla nainstalovat do Windows 95.
DirectX 8.1
12. 11. 2001
Určeno i pro verzi Windows XP.
DirectX 9.0
19. 12. 2002
DirectX 9.0a
26. 3. 2003
DirectX 9.0b
13. 8. 2003
DirectX 9.0c
13. 12. 2004
DirectX 10
30. 11. 2006
DirectX 4.0
Nikdy nebylo vydáno.
Dvě verze – pro Windows 95 a Windows 98.
Určeno pouze pro Windows Vista.
Z této tabulky je patrné, že předposlední oficiální DirectX je verze 9.0c. Nicméně již výše jsme si uvedli, že každé dva měsíce vznikají nové aktualizace DirectX SDK, které samozřejmě neovlivňují uživatele aplikací, ale především vývojáře. Postupně se kromě podpory 32bitových programů objevila i podpora pro 64bitové aplikace a v několika posledních verzích SDK se objevily i hlavičkové soubory s knihovnami a příklady pro nadcházející DirectX 10. Tvorbou aplikací pro DirectX 10 se v této knize zabývat nebudeme. Přesto se alespoň krátce zmiňme o této (prozatím nejnovější) verzi. V době psaní této knihy (léto 2007) je přímou součástí Windows Vista – pro předchozí verze Windows tedy není k dispozici. V každém případě ale DirectX 10 přinášejí řadu podstatných změn, mezi nimiž nechybí kompletně přepracované API, jeho rozdělení do dvou částí (smyslem je zvýšit stabilitu systému), celkové urychlení nebo unifikovaná podpora shaderů. Změn a rozšíření je pochopitelně více, a jak dokazují první aplikace, které DirectX 10 využívají, zejména v oblasti počítačové grafiky dochází k velkému posunu vpřed. V budoucnosti se tak tato verze jistě dočká masivnějšího rozšíření. Pohled do historie DirectX zakončíme odstavcem věnujícím se vzájemné kompatibilitě jednotlivých verzí. U všech verzí kromě nejnovějších DirectX 10 platí, že jsou zpětně kompatibilní. V praxi to znamená, že pokud máte například nainstalované DirectX 8.0, spustíte i aplikaci, která byla vyvíjena pro všechna starší DirectX (např. DirectX 6.1). Toto je výhoda modelu COM, na němž je DirectX postaven. Obdobně to platí i při vývoji aplikací. V novějších verzích DirectX SDK naleznete i hlavičkové soubory a knihovny starších verzí. Toto se mění u DirectX 10, jež by mělo být kompatibilní pouze s předposledními verzemi – DirectX 7, 8 a 9. Protože bylo předělané celé API, tato kompatibilita je dána tak, že se starší rozhraní pouze emulují. To znamená, že drtivá většina aplikací, jež využívají starší rozhraní, je sice funkční, ale některé testy ukazují, že takové aplikace mohou být pod DirectX 10 v řádu několika procent pomalejší.
1. Bližší seznámení s DirectX
Prodáno 14.10.2014 na www.Kosmas.cz zákazníkovi
[email protected], číslo objednávky 591522, UID: 7e15c933-e4b8-4e2a-94a6-c3fd2dffdec9
2.
15
2. Základní aplikace
DIRECTX
Základní aplikace Po úvodním seznámení s DirectX se dostáváme k programování. V této kapitole ještě nebudeme vytvářet DirectX aplikace. Její náplní je jednak popis způsobu nastavení prostředí a projektu v Microsoft Visual C++ a vytvoření základní Win32 aplikace. Z takové aplikace budeme vytvářet v dalších částech této knihy programy, které již DirectX budou využívat. Poslední část této kapitoly je věnována způsobu, jak zjistit verzi DirectX, která je v počítači instalována.
2.1 Nastavení DirectX pro naše aplikace Než si popíšeme tvorbu projektu pro DirectX aplikace, je třeba mít nainstalovaný DirectX end-user runtime ve verzi, pro kterou budete aplikace vytvářet (v současné době je to DirectX 9.0c) [14]. V opačném případě by nešly takové aplikace spustit. Tento programový balík vyžadují všechny aplikace, které jsou založeny na DirectX. Dále – pokud ještě nemáte – stáhněte si a nainstalujte DirectX SDK (o tomto balíčku jsme se zmínili již v úvodu). Nyní však již k vytvoření projektu. Následující popis se vztahuje k Microsoft Visual C++ 6.0.
2.1 Nastavení DirectX pro naše aplikace
Prodáno 14.10.2014 na www.Kosmas.cz zákazníkovi
[email protected], číslo objednávky 591522, UID: 7e15c933-e4b8-4e2a-94a6-c3fd2dffdec9
16
DIRECTX
Obrázek 2.1: Okno Visual C++ Express Edition pro nastavení nové aplikace
Po spuštění Visual C++ zadáme příkaz File → New a poté Project. V okně projektů si vybereme Win32 Application, doplníme jméno projektu a případně si nastavíme cestu do adresáře na disku, kde ho chceme vytvořit. Po stisknutí tlačítka OK se dostaneme do další nabídky, kde si můžeme zvolit, jaký typ aplikace Windows chceme vytvořit. Zaškrtneme volbu An Empty Project a klepneme na tlačítko Finish, čímž návrh projektu dokončíme. Objeví se ještě jedno okno, kde vidíme souhrn námi provedených nastavení. Po dalším stisknutí tlačítka OK je projekt vytvořen. V prostředí Visual C++ Express Edition postupujeme obdobně. Tedy po příkazu File → New → Project se opět dostaneme do okna vytvoření projektu. Zde nemáme k dispozici volbu Win32 Application, proto si musíme vybrat Win32 Console Application. Opět doplníme název projektu a vybereme jeho umístění na disku v počítači. V dalším okně máme dvě nabídky – Overview a Application Settings. V nabídce Overview máme přehled o nastavení projektu. Nás ale zajímá druhá nabídka. Pokud se tedy přepneme do Application Settings, můžeme si vybrat typ aplikace (Application type – zde zaškrtněte volbu Windows application) a v Additional options zaškrtněte volbu Empty project (prázdný projekt). Toto okno vidíte na obrázku 2.1. Poté již stačí pouze klepnout na tlačítko Finish a projekt je vytvořen. Možná se v tuto chvíli ptáte, proč jsme vytvářeli prázdný projekt. Kdybychom si nevybrali volbu Empty project, průvodce vytvoření projektu by počáteční aplikaci Windows vytvořil za nás. To je sice pravda, ale takto vytvořená aplikace nebude objektová, a navíc pokud máme vytvořený prázdný projekt, nic v něm není. V tom projektu bude tedy vždy jen to, co do něj sami vložíme. A konečně, tuto základní aplikaci si ve zbytku této kapitoly popíšeme, takže při jejím vytváření lépe pochopíte, jak vlastně funguje. Projekt tedy máme vytvořený. Než začneme programovat, musíme ještě provést jistá nastavení. Těmito nastaveními je myšleno nastavení cesty do adresářů, odkud se budou načítat hlavičkové a knihovní soubory DirectX (soubory s příponou *.h a *.lib). V Microsoft
2. Základní aplikace
Prodáno 14.10.2014 na www.Kosmas.cz zákazníkovi
[email protected], číslo objednávky 591522, UID: 7e15c933-e4b8-4e2a-94a6-c3fd2dffdec9
DIRECTX
17
Visual C++ 6.0 toto naleznete v nabídce Tools → Options na kartě Directories. Zde pro hlavičkové soubory (Include files) a knihovní soubory (Library files) nastavte cesty do adresářů Include a Lib na disku, kam jste nainstalovali DirectX SDK. Ve Visual C++ Express Edition naleznete stejné nastavení v položkách Tools → Options ve složkách Projects and Solutions a VC++ Directories. Úplně poslední věcí, kterou při nastavení projektu můžeme (ale nemusíme) provést, je nastavení knihoven, které se budou přilinkovávat k našemu programu. Toto nastavení provádět nemusíme, protože do zdrojového kódu našeho programu můžeme někam vložit direktivu například v tomto znění:
Potom to bude mít stejný účinek, jako bychom soubor dxguid.lib přidali do seznamu přilinkovávaných knihoven našeho projektu. Stejným způsobem bychom přidávali i jiné knihovny (o tom, které knihovny jsou kdy potřeba, si povíme na začátku jednotlivých kapitol o částech DirectX). Samozřejmě pokud máme nastavenou cestu ke knihovnám tak, jak jsme si uvedli výše, stačí pouze uvádět jméno souboru a nemusíme psát celou cestu k němu. Pokud chceme provést nastavení přilinkovávaných knihoven přímo v projektu, ve Visual C++ 6.0 se to dělá příkazem Project → Settings pro typ projektu (standardně Debug nebo Release) na kartě Link. Ve formulářovém poli Object/library modules je již několik automaticky přidaných knihoven. Stačí k nim tedy připojit jméno souboru, který chceme přidat.
2. Základní aplikace
#pragma comment (lib,"d:\\Dx\\Lib\\dxguid.lib")
Obrázek 2.2: Okno Visual C++ Express Edition pro přidání přilinkovávaných knihoven
Ve Visual C++ Express Edition se knihovny do projektu přidávají v nabídce Project → Properties. Objeví se okno, v jehož levé části se nachází rozbalovací stromová struktura s několika rozbalovacími složkami. Nás konkrétně zajímá složka Configuration Pro-
2.1 Nastavení DirectX pro naše aplikace
Prodáno 14.10.2014 na www.Kosmas.cz zákazníkovi
[email protected], číslo objednávky 591522, UID: 7e15c933-e4b8-4e2a-94a6-c3fd2dffdec9
18
DIRECTX
perties – Linker – Command Line (viz obrázek 2.2). Pro zvolenou konfiguraci v horní části tohoto okna (Debug, Release, nebo nějaká vlastní) se do pole Additional options zapisují přilinkovávané knihovny. Jednotlivé zapsané soubory se oddělují mezerou.
2.2 Vytvoření základní Win32 aplikace Dříve, než si vytvoříme základní Win32 aplikaci, si uvedeme několik informací o koncepci takových programů. Doporučuji tedy prostudovat a zamyslet se nad následujícími řádky především těm z vás, kteří dosud žádný Win32 program netvořili. Usnadní vám to porozumění principům tvorby Win32 aplikací. Tato koncepce je od klasických konzolových programů poněkud odlišná. Pochopení tvorby takových aplikací nám usnadní zamyšlení nad chováním platformy Windows. Jak jistě víte, jde o grafický operační systém, přičemž v jednom okamžiku můžeme mít spuštěno více aplikací. To znamená, že potřebujeme mít určitým způsobem rozdělenou paměť. Jednotlivé programy také potřebují sdílet nejrůznější zařízení, neboť stejné zařízení může využívat i jiná aplikace. Z těchto důvodů je programování Windows založeno na tzv. událostech. Událostí je například pohyb myši, klepnutí některého tlačítka myši, stisknutí některé klávesy nebo změna velikosti okna aplikace. V programu se dají jednotlivé události rozlišit, proto můžeme zajistit reakci na každou událost zvlášť. Samozřejmě, že také můžeme nechat bez povšimnutí události, která nás nezajímají. Události můžeme v našem programu zpravidla zachytit podle zpráv, které Windows posílají vždy konkrétnímu oknu aplikace, pro niž je určena. Příslušné okno potom může na konkrétní zprávu zareagovat. Pro nás jako programátory je nejdůležitější vědět, že musíme napsat funkci, které se říká procedura okna. Uvnitř ní definujeme zprávy, které nás zajímají, a dále pak to, jakým způsobem na ně budeme chtít reagovat. O posílání zpráv této funkci se nemusíme starat, to udělají Windows za nás. Shrnutí těchto poznatků o tvorbě základní Win32 aplikace je tedy následující: po spuštění programu se nejprve registruje třída okna, jemuž definujeme požadované vlastnosti. Na základě této registrace se vytvoří a vykreslí okno Windows aplikace. Potom již probíhá smyčka zpráv, vybírající z fronty ty zprávy, které se týkají naší aplikace, jež se následně předávají proceduře okna, kde můžeme definovat reakce. Zmíněná smyčka zpráv probíhá cyklicky stále dokola, dokud není vyzvednutá zpráva WM_QUIT (viz dále) udávající, že má aplikace končit. To je vše. Jistě uznáte, že na základě předchozího popisu nevypadá princip činnosti programů pro Windows složitě. V tuto chvíli si tedy projděme celý programový kód, který nám tuto aplikaci vytvoří. Jak bylo zmíněno v úvodu knihy, všechny programy, včetně tohoto, budou vytvářeny objektově. V nové Win32 aplikaci tedy vytvoříme šest souborů. První z nich bude obsahovat globální prvky naší aplikace, můžeme ho proto nazvat například Global. h. Další soubor WinMain.cpp bude obsahovat hlavní smyčku programu, včetně zpracování smyčky zpráv. V tomto souboru se vytvoří instance třídy CApplication a následně se provede zavolání metody jejího vytvoření. Tato třída se bude nacházet v souboru CApplication.h a definice jejích metod v souboru CApplication.cpp. Funkce třídy CApplication je chápána ve smyslu správy celé aplikace. Pro práci s oknem aplikace nám poslouží třída CWindow, kterou umístíme do souboru CWindow.h a jejíž metody budou definovány v souboru CWindow.cpp. Nyní k jednotlivým souborům podrobněji. Obsah souboru Global.h vypadá následovně:
2. Základní aplikace
Prodáno 14.10.2014 na www.Kosmas.cz zákazníkovi
[email protected], číslo objednávky 591522, UID: 7e15c933-e4b8-4e2a-94a6-c3fd2dffdec9
DIRECTX
19
#pragma once #include <windows.h> #define APP_NAME TEXT("Moje aplikace") Uvnitř tohoto souboru se nacházejí pouze tři příkazy. Direktiva #pragma once zajistí, že se tento soubor bude k projektu přidávat pouze jednou, přestože je vložen do jiných souborů vícekrát. Poté vkládáme hlavičkový soubor windows.h, čímž si zajistíme možnost využívání proměnných, struktur a funkcí Win32 API. Poslední příkaz přiřazuje symbolické konstantě APP_NAME text "Moje aplikace". Jde o text obsahující název aplikace, který se zobrazí v záhlaví našeho okna. Makro TEXT nám říká, že jde o textový řetězec v podobě mezinárodního kódování UNICODE.
#include "Global.h" #include "CApplication.h" int WINAPI WinMain(HINSTANCE, HINSTANCE, LPSTR, int) { CApplication App; MSG msg;
2. Základní aplikace
Nyní si popíšeme strukturu souboru Winmain.cpp. Jak bylo zmíněno výše, v tomto souboru začíná „život“ celé aplikace, a vlastně zde i končí.
if(!App.Initialize()) { MessageBox (NULL, TEXT ("Chyba při inicializaci!"), APP_NAME, MB_ICONERROR) ; return (0); } while(true) { if(PeekMessage(&msg, NULL, 0, 0, PM_REMOVE)) { if(msg.message == WM_QUIT) break; TranslateMessage(&msg); DispatchMessage(&msg); } // Zde mohou být volány funkce našeho programu } return (msg.wParam); } Na počátku vkládáme dvojici hlavičkových souborů – Global.h a CApplication.h. První, jak víme, obsahuje globální příkazy pro aplikaci, druhý obsahuje třídu CApplication, jejíž instanci v hlavní smyčce programu vytvoříme. Zatímco u konzolových aplikací se hlavní funkce programu jmenuje main, zde se jmenuje WinMain. Vrací hodnotu typu int a pokud se podíváme na konec programu, vrací se msg.wParam, což je primární parametr zprávy (viz dále). Funkce WinMain se spouští se čtveřicí parametrů. Můžete si všimnout,
2.2 Vytvoření základní Win32 aplikace
Prodáno 14.10.2014 na www.Kosmas.cz zákazníkovi
[email protected], číslo objednávky 591522, UID: 7e15c933-e4b8-4e2a-94a6-c3fd2dffdec9
20
DIRECTX
že v našem výpisu kódu jsou uvedeny pouze jejich typy a nikoliv identifikátory. To je proto, že je nebudeme potřebovat. Nicméně alespoň ve stručnosti si povězme, že první dva parametry jsou typu HINSTANCE, což ve znamená handle instance. Handle představuje číslo a první parametr je tedy číslo, které jednoznačně identifikuje program (některé funkce tento parametr mohou vyžadovat). Druhý parametr je stejného typu a je to jakýsi pozůstatek ze starších verzí Windows, kdy se při vícenásobném spuštění programu sdílela paměť pro čtení. Je to tedy handle předchozí instance programu. Třetím parametrem je příkazový řádek, který se používá pro spuštění programu (to samé, co může mít i funkce main). A poslední (čtvrtý) parametr udává, jak má být program poprvé zobrazen – okno programu může být zobrazeno normálně, maximalizované nebo minimalizované. V hlavní smyčce programu vytváříme instanci třídy CApplication s názvem App a dále vytváříme identifikátor msg na základě struktury zprávy MSG. Ten budeme potřebovat pro zpracování zpráv Windows. Potom je volána metoda Initialize() instance App, která provádí inicializaci včetně vykreslení okna. Podrobnostmi této metody se budeme zabývat v dalším textu. V tuto chvíli je pro nás ale podstatné to, že tato metoda vrací binární hodnotu (bool) v závislosti na tom, zda inicializace proběhla v pořádku. Pokud ne, zobrazí se dialogové okno, že došlo k chybě (funkce MessageBox) a program skončí. Prvním parametrem této funkce je obvykle handle okna (v našem případě žádné není – NULL), druhým je textový řetězec, který se objeví uvnitř dialogového okna, třetím parametrem pak je text v záhlaví tohoto okna (makro APP_NAME). Čtvrtý argument je kombinace konstant, které nám říkají, jaká tlačítka a ikony se mají v tomto dialogu objevit. V našem programu zobrazujeme chybovou ikonu danou symbolickou konstantou MB_ICONERROR. Zbylou část programu tvoří nekonečný cyklus obsahující dvě části. V první části se pracuje se zprávami Windows a druhou část máme v našem programu prázdnou. Jak napovídá komentář, zde se vkládají funkce a algoritmy, které chceme, aby náš program vykonával. Popišme si podrobněji první část tohoto cyklu. Funkce Peekmessage zjišťuje, zda se ve frontě nachází zpráva určená oknu programu. Pokud ano, umístí ji do naší struktury msg a vrátí hodnotu true (jinak vrací false). Protože máme volání této funkce v podmínce, v případě jejího splnění se provede tělo této podmínky. Druhým parametrem funkce Peekmessage je handle okna, které zprávy přijímá (v našem případě NULL) a třetí a čtvrtý parametr udávají meze zpráv, jež se mají načítat (minimální a maximální). My zde máme uvedeny hodnoty 0, protože chceme načítat všechny zprávy. U posledního parametru zde máme hodnotu PM_REMOVE, která udává, že mají být tyto zprávy po načtení z fronty odstraněny. Pokud naše okno aplikace obdrží nějakou zprávu, proběhne kód uvnitř zmíněné podmínky. Jako první zde máme další podmínku. Jejím smyslem je zjistit, zda zpráva, která byla přijata, není náhodou WM_QUIT. Jak z jejího názvu vyplývá, jde o zprávu, která oznamuje, že má program skončit. Pokud je tato zpráva zaslána, celý (nekonečný) cyklus while se přeruší a program skončí. V opačném případě proběhne ještě volání dvou funkcí a cyklus bude pokračovat dál. První z těchto funkcí je TranslateMessage. Tato funkce předává strukturu msg (její parametr) zpět Windows kvůli klávesnicovým převodům. Druhá funkce (DispatchMessage) předává strukturu msg opět Windows, tentokrát ovšem Windows tuto zprávu zašlou proceduře okna, kde na ní můžeme reagovat (viz dále). Soubor CApplication.h obsahuje následující kód: #pragma once #include "CWindow.h"
2. Základní aplikace
Prodáno 14.10.2014 na www.Kosmas.cz zákazníkovi
[email protected], číslo objednávky 591522, UID: 7e15c933-e4b8-4e2a-94a6-c3fd2dffdec9
DIRECTX
21
class CApplication { private: CWindow m_Window; public: CApplication(void); ~CApplication(void); bool Initialize(void); void Terminate(void); Obsahem souboru CApplication.h je tedy třída, která má stejný název jako soubor. Úkolem této třídy je správa aplikace a jejích algoritmů. V našem případě tu máme pouze konstruktor a destruktor, metody Initialize a Terminate. Smyslem metody Initialize je provést inicializaci naší aplikace (jak víme, tato metoda se volá z hlavní smyčky programu Winmain.cpp). Naopak Terminate bude provádět ukončovací operace. Kromě těchto metod třída pouze agreguje instanci třídy CWindow s názvem m_Window pro práci s oknem našeho programu (proto na začátku musíme vkládat hlavičkový soubor CWindow.h). Tato třída je jednoduchá, protože máme jednoduchou aplikaci. S rozsáhlejší aplikaci by samozřejmě úměrně rostla složitost této třídy (práce se zvuky a hudbou, umělou inteligencí programu apod.).
2. Základní aplikace
};
Soubor CApplication.cpp obsahuje definované metody třídy CApplication. Výpis je následující: #include "Global.h" #include "CApplication.h" CApplication::CApplication(void) { } CApplication::~CApplication(void) { Terminate(); } bool CApplication::Initialize(void) { if(!m_Window.Initialize()) return (false); return (true); } void CApplication::Terminate(void) { } Po vložení potřebných hlavičkových souborů zde máme konstruktor a destruktor. Zatímco konstruktor je prázdný, z destruktoru se volá metoda Terminate. Ani jedna z těchto tří metod by tu nemusela být, ale u každé třídy je lepší je (byť prázdné) doplnit s ohledem na přehlednost a snadnější implementace programových algoritmů při případném budoucím rozšíření programu. Poslední metodou je Initialize. Protože naše aplikace vlastně jen
2.2 Vytvoření základní Win32 aplikace
Prodáno 14.10.2014 na www.Kosmas.cz zákazníkovi
[email protected], číslo objednávky 591522, UID: 7e15c933-e4b8-4e2a-94a6-c3fd2dffdec9
22
DIRECTX
vytváří okno programu, v těle této metody voláme u instance Window metodu Initialize. V případě úspěšného vytvoření okna aplikace se vrací hodnota true, v opačném případě false. Nyní se dostáváme k jádru našeho programu. Půjde o vytvoření okna a o funkci zpracování zpráv. Oboje nalezneme v souborech CWindow.h a CWindow.cpp. Soubor CWindow.h obsahuje následující kód: #pragma once class CWindow { private: HWND m_hWnd; public: CWindow(void); ~CWindow(void); bool Initialize(void); friend LRESULT CALLBACK WndProc(HWND m_hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam); }; Třída CWindow má jediný atribut – m_hWnd, což je proměnná typu HWND. Je to vlastně zkratka znamenající handle window, tedy identifikátor (číslo) okna. Tuto proměnnou používá mnoho funkcí Win32 API (ale nejen těch), protože se díky ní určuje, jakého okna aplikace se příslušná funkce týká – každé okno má tuto hodnotu jedinečnou. Atribut m_hwnd máme ve třídě, neboť ho potřebujeme při vytváření okna (metoda Initialize) i u funkce zpracování zpráv zaslaných oknu aplikace (WndProc). Všimněte si, že funkce WndProc není členskou metodou této třídy, ale jde o globální funkci, která je s touto funkcí spřátelená (friend). To proto, že potřebuje přímý přístup k m_hWnd (má ho hned jako první parametr). Výpis posledního souboru (CWindow.cpp) je nejdelší. Jsou zde definované všechny metody třídy CWindow a také tělo funkce WndProc: #include "Global.h" #include "CWindow.h" CWindow::CWindow(void) { m_hWnd = NULL; } CWindow::~CWindow(void) { } bool CWindow::Initialize(void) { WNDCLASS wc;
2. Základní aplikace
Prodáno 14.10.2014 na www.Kosmas.cz zákazníkovi
[email protected], číslo objednávky 591522, UID: 7e15c933-e4b8-4e2a-94a6-c3fd2dffdec9
DIRECTX
23
ZeroMemory(&wc, sizeof(wc)); wc.style = CS_HREDRAW|CS_VREDRAW; wc.lpfnWndProc = WndProc; wc.hInstance = GetModuleHandle(NULL); wc.hIcon = LoadIcon(NULL, IDI_WINLOGO); wc.hCursor = LoadCursor(NULL, IDC_ARROW); wc.hbrBackground = (HBRUSH) GetStockObject(WHITE_BRUSH); wc.lpszClassName = APP_NAME; if(!RegisterClass(&wc)) return (false);
ShowWindow(m_hWnd, SW_SHOW); UpdateWindow(m_hWnd); return (true); }
2. Základní aplikace
if(!(m_hWnd = CreateWindowEx(0, APP_NAME, APP_NAME, WS_OVERLAPPEDWINDOW, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, NULL, NULL, wc.hInstance, NULL))) return (false);
LRESULT CALLBACK WndProc(HWND m_hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam) { switch(uMsg) { case WM_KEYDOWN: switch(wParam) { case VK_ESCAPE: PostQuitMessage(0); break; } break; case WM_DESTROY: PostQuitMessage(0); break; } return DefWindowProc(m_hWnd, uMsg, wParam, lParam); } Po vložení hlavičkového souboru se v tomto souboru nachází těla konstruktoru a destruktoru. V konstruktoru inicializujeme m_hWnd na hodnotu NULL (nulu, protože zatím žádné okno nemáme). Poté následuje tělo metody Initialize. Nejdříve vytváříme instanci struktury WNDCLASS s názvem wc. Tato instance je potřeba pro registrování třídy okna a musí být naplněna určitými informacemi o vytvářeném okně aplikace. Tyto údaje jsou uvedeny v dalších řádcích této metody. Ještě předtím je ovšem oblast paměti, která byla vyhrazena instanci wc, vymazána, aby zde nebyly umístěny nežádoucí hodnoty (funkce ZeroMemory).
2.2 Vytvoření základní Win32 aplikace
Prodáno 14.10.2014 na www.Kosmas.cz zákazníkovi
[email protected], číslo objednávky 591522, UID: 7e15c933-e4b8-4e2a-94a6-c3fd2dffdec9
24
DIRECTX
Nyní již k jednotlivým parametrům struktury WNDCLASS. Položka style má přiřazeny identifikátory CS_HREDRAW a CS_VREDRAW. Jde o bitové příznaky, které udávají, že se okno aplikace překreslí vždy, když se změní jeho velikost v horizontálním (CS_HREDRAW) nebo vertikálním (CS_HREDRAW) směru. Druhou položkou je lpfnWndProc. To je ukazatel na funkci, kde se zpracovávají zprávy určené právě tomuto oknu. Naše funkce se jmenuje WndProc. Třetím parametrem je hInstance, což je handle instance programu. Zpravidla se tento údaj shoduje s prvním parametrem hlavní funkce WinMain. My jsme tento parametr uveden neměli, proto voláme funkci GetModuleHandle, která tuto hodnotu zjistí. Následuje parametr hIcon. Jde o handle ikony, která bude reprezentovat toto okno programu. Funkce LoadIcon ikonu načte (příslušná ikona se zobrazuje před názvem ve stavovém řádku). Protože používáme některou z předdefinovaných ikon, první parametr této funkce je NULL a druhý je identifikátor této ikony. Podobný smysl jako hIcon má i další parametr – hCursor. Tentokrát ovšem jde o handle ikony kurzoru myši, který se zobrazí, pokud je tento kurzor umístěn nad oknem programu. Pro načtení kurzoru myši se používá funkce LoadCursor a význam jejích parametrů je obdobný jako u funkce LoadIcon. Předposlední položkou, kterou u instance wc udáváme, je hbrBackground. Jde o handle ke „štětci“, jímž se vybarvuje barva pozadí okna. Pro nastavení tohoto údaje používáme funkci GetStockObject, jež má parametr WHITE_BRUSH (bílý štětec). Můžete tento parametr změnit například na BLACK_BRUSH a po opětovné kompilaci programu zaznamenáte změnu barvy pozadí na černou. Posledním parametrem instance wc je lpszClassName, což představuje název třídy, která se bude v dalším kroku vytvářet. Zde přiřazujeme název třídy makrem APP_NAME (jak víme, toto makro je definováno v souboru Global.h). V dalším kroku se volá funkce RegisterClass. Tato funkce vytváří novou třídu okna, která je nutná pro vytvoření tohoto okna. Parametrem je reference na instanci struktury typu WNDCLASS. Proto jsme předtím museli vytvořit instanci (wc) a nastavit její hodnoty. Pokud se registrace nepodaří, vrací se hodnota 0. Po registraci třídy okna můžeme toto okno vytvořit. To provádí funkce CreateWindowEx. Pokud neuspěje, vrací hodnotu 0, v opačném případě vrací handle tohoto okna, které uložíme do instance třídy (atribut m_hWnd). První parametr funkce CreateWindowEx je rozšířený styl k vytvoření okna. Protože žádný nepotřebujeme, máme zde hodnotu 0. Druhým parametrem je jméno třídy, tedy stejný údaj, který jsme uvedli v příkazu wc.lpszClassName. Třetí argument je jméno okna (bude uvedeno v jeho záhlaví). Opět zde používáme makro APP_NAME. Následuje styl okna. Hodnota WS_OVERLAPPEDWINDOW udává, že půjde o klasické okno se záhlavím a ohraničením, u okna půjde měnit velikost a bude obsahovat tlačítka pro minimalizaci a maximalizaci. Následující čtveřice parametrů má dosazené hodnoty CW_USEDEFAULT. V pořadí, jak jsou uvedeny za sebou, mají následující význam: horizontální pozice levého horního rohu okna, vertikální pozice levého horního rohu okna, horizontální šířka okna a vertikální šířka okna. Všechny údaje jsou uvedeny v pixelech. Tím, že máme uvedeny parametry CW_USEDEFAULT, udáváme, že nám na pozici a velikosti okna po spuštění nezáleží (dosadí se standardní hodnoty). Následuje argument, kam se zapisuje handle k rodičovskému oknu. Protože žádné takové okno nemáme, je zde uvedena hodnota NULL. Hodnota NULL je i v dalším parametru. Zde je handle k nabídce okna a NULL udává, že se použije třída nabídky. Předposledním parametrem je handle instance programu. Toto jsme již měli uloženo u položky hInstance u instance wc, proto je zde uvedeno wc.hInstance. Posledním argumentem je ukazatel na data, která jsou předávána v parametrech zprávy WM_CREATE (při spuštění programu). Toto nevyužíváme, proto zde máme uvedenou hodnotu NULL. Okno máme vytvořeno. Zbývá jeho nastavení a zobrazení. To provádí funkce ShowWindow. Jejím prvním parametrem je handle okna, jehož se toto nastavení týká, druhý parame-
2. Základní aplikace
Prodáno 14.10.2014 na www.Kosmas.cz zákazníkovi
[email protected], číslo objednávky 591522, UID: 7e15c933-e4b8-4e2a-94a6-c3fd2dffdec9
DIRECTX
25
tr je kombinací hodnot udávajících způsob zobrazení. My zde máme uvedeno SW_SHOW, což znamená, že okno bude zobrazeno ve své aktuální velikosti a umístění. Poslední funkcí v metodě Initialize je UpdateWindow, které nám zajistí překreslení klientské části okna. O jaké okno se jedná, je dáno jeho jediným parametrem (handle okna).
Tělo funkce WndProc tvoří vícenásobné rozvětvení programu, řešené nejčastěji příkazem switch. Tímto způsobem zprávy roztřídíme a na každou z nich můžeme určitým způsobem reagovat. Samozřejmě můžeme ošetřit reakce pouze na zprávy, které nás zajímají. Nicméně všechny zprávy (tedy i ty, co nás nezajímají) jsou funkcí DefWindowProc zpracovány standardním způsobem, který zajišťuje operační systém. Toto je také velice důležitá část programu, protože by neměly zůstat žádné nezpracované zprávy. Všimněte si, že tato funkce má stejné parametry jako WndProc.
2. Základní aplikace
Poslední částí programu, kterou nám ještě zbývá popsat, je funkce pro zpracování zpráv – WndProc. Takovou funkci má ve Windows k dispozici každé okno programu a tato funkce je volána operačním systémem – viz funkci DispatchMessage, kterou jsme si popsali výše. Také jsme dříve uvedli, že pro zpracování zpráv je tato funkce propojena s třídou okna (viz příkaz wc.lpfnWndProc = WndProc), takže jednu funkci může sdílet i více oken. Z hlavičky funkce WndProc vidíme, že má čtveřici parametrů. Prvním je handle okna, kterému je zpráva zaslána, uMsg je identifikátor zprávy, zbylé dva datové typy udávají primární (wParam) a sekundární (lParam) parametry zprávy. Pokud je například stisknuta klávesa, z parametrů lze zjistit, která to byla.
Ještě se podívejme dovnitř funkce WndProc, jaké zprávy zpracováváme. Zpráva WM_KEYDOWN se vyvolá v okamžiku, kdy je stisknuta nějaká klávesa. Kód této klávesy je uložen v proměnné wParam. V našem programu máme podmínku, že pokud byla stisknuta klávesa ESCAPE (identifikátor VK_ESCAPE), zavolá se funkce PostQuitMessage (0). Tato funkce vygeneruje zprávu WM_QUIT, o které jsme si již dříve řekli, že způsobí konec programu. Jinými slovy, stisknutím klávesy ESCAPE se okno aplikace zavře a program se ukončí. Stejný účinek má i zpráva WM_DESTROY. Tato zpráva je generována tehdy, když myší klepneme na ikonu kříže v pravém horním rohu okna aplikace, nebo stiskneme klávesovou zkratku ALT+F4 (tímto způsobem se standardně ukončují programy Windows). Popsali jsme si tedy základní aplikaci Windows. Věřím, že tento popis byl dostatečně srozumitelný. V dalších kapitolách budeme z této aplikace vycházet a budeme k ní přidávat další části programových algoritmů, tentokrát již s komponentami DirectX. Ještě než k tomu dojde, ukážeme si velice jednoduchý program, který zjistí, jaká verze DirectX je v počítači instalována.
2.3 Zjištění verze DirectX Uvedli jsme si, že DirectX je zpětně kompatibilní. Nicméně se může stát, že budeme vyvíjet program pro nové rozhraní. Pokud bude mít uživatel instalováno starší rozhraní, nebude program fungovat. Dopředná kompatibilita pochopitelně neexistuje. V takových případech je užitečné zjistit, jaká verze DirectX je v počítači nainstalována a pokud bude starší než ta, pro kterou je program vytvořen, nechat o tom zobrazit zprávu, aby si uživatel DirectX end-user runtime aktualizoval – a pak program skončí. Zjistit verzi DirectX je otázkou jediné funkce DirectXSetupGetVersion. Potom již stačí získané údaje jen zpracovat. Kód celého programu pro zjištění verze DirectX vypadá následovně: #define _CRT_SECURE_NO_DEPRECATE 1
2.3 Zjištění verze DirectX
Prodáno 14.10.2014 na www.Kosmas.cz zákazníkovi
[email protected], číslo objednávky 591522, UID: 7e15c933-e4b8-4e2a-94a6-c3fd2dffdec9
26
DIRECTX
#include <windows.h> #include <stdio.h> #include
#pragma comment (lib,"dsetup.lib") int WINAPI WinMain(HINSTANCE, HINSTANCE, LPSTR, int) { DWORD version; DWORD revision; TCHAR buffer[256]; if (DirectXSetupGetVersion(&version, &revision)) { sprintf(buffer,TEXT("DirectX verze je %d.%d.%d.%d\n"), HIWORD(version), LOWORD(version), HIWORD(revision), LOWORD(revision)); MessageBox(NULL,buffer, TEXT("Zjištění verze DirectX"), MB_OK); } else { MessageBox(NULL, TEXT("DirectX není nainstalováno !"), TEXT("Zjištění verze DirectX"), MB_OK); } return(0); } Na začátku programu máme vloženou symbolickou konstantu _CRT_SECURE_NO_DEPRECATE s hodnotou 1. Toto makro má pouze jediný účel: v programu používáme funkci sprintf, která pro kompilátor Visual C++ Express Edition není příliš bezpečná. V tomto kompilátoru zmíněné makro pouze potlačuje varovné hlášení. Potom vkládáme trojici hlavičkových souborů. První z nich je windows.h, druhý je stdio.h (kvůli funkci sprintf) a poslední je knihovna DirectX dsetup.h, v níž je definována funkce DirectXSetupGetVersion. Abychom mohli vytvořit spustitelný program, musíme ke zkompilovanému programu přilinkovat knihovnu dsetup.lib. Funkce DirectXSetupGetVersion má dva argumenty – reference na proměnné typu DWORD. V případě úspěšného volání se vrací nenulová hodnota. Do prvního parametru se uloží číslo verze a do druhého číslo revize. Pokud některý z těchto údajů nepotřebujeme, může se nastavit jako NULL. V případě neúspěchu (DirectX není nainstalováno) se vrací hodnota 0. V obou případech se na obrazovce zobrazuje informační okno (Win32 funkce MessageBox). Makra TEXT zajistí, aby zobrazené textové řetězce byly v kódování UNICODE, tedy aby se text správně zobrazoval v různých jazykových verzích Windows. Pokud zjistíme čísla verze a revize DirectX, pomocí funkce sprintf se tato čísla společně s doprovodným textem uloží do textového pole buffer (opět jako UNICODE).
2. Základní aplikace
Prodáno 14.10.2014 na www.Kosmas.cz zákazníkovi [email protected], číslo objednávky 591522, UID: 7e15c933-e4b8-4e2a-94a6-c3fd2dffdec9
DIRECTX
27
Tabulka 2.1: Hodnota proměnné version pro různé verze DirectX. Zdroj: [14]
Verze DirectX
Obsah proměnné version
DirectX 5
0x00040005
DirectX 6
0x00040006
DirectX 7
0x00040007
DirectX 8
0x00040008
DirectX 9
0x00040009
Pokud máte nainstalovaný například DirectX 9.0c, program zobrazí text DirectX verze je 4.9.0.904. Možná se vám zdá toto označení matoucí. Obě hodnoty (číslo verze a revize) se skládají ze dvou částí. Proto se nám zobrazuje čtveřice čísel oddělených tečkou. Vysvětlení těchto čísel najdeme v dokumentaci k této funkci v DirectX SDK [14]. Tabulka 2.1 udává hodnotu proměnné version pro různé verze DirectX. Tabulka 2.2: Hodnota proměnné revision pro různé verze DirectX. Zdroj: [14]
DirectX release
Obsah proměnné revision
DirectX 9
0x00000384 (4.09.00.0900)
DirectX 9a
0x00000385 (4.09.00.0901)
DirectX 9b
0x00000386 (4.09.00.0902)
DirectX 9c
0x00000387 (4.09.00.0903)
DirectX 9c
0x00000388 (4.09.00.0904)
Poznámky
2. Základní aplikace
Po zkompilování programu a následném spuštění by měl program fungovat. Může se ale stát, že vám oznámí, že mu chybí knihovna DSETUP.dll. Tento soubor je součástí DirectX SDK. Naleznete ho v adresáři Redist. Stačí ho tedy jen zkopírovat do adresáře, v němž se nachází tento program.
aktualizované DirectX 9b
Odtud tedy pochází první dvojice čísel, kterou nám program zobrazí (číslo ve druhém sloupci je v šestnáctkové soustavě). Podobně je na tom číslo revize. To se skládá z čísel buildu (sestavení) a release (uvolnění). Pro verze DirectX9 zachycuje čísla revizí tabulka 2.2. Je to ta druhá dvojice čísel, která se zobrazuje. Čísla v závorce ve druhém sloupci jsou pak čísla verze a revize v desítkové soustavě.
2.3 Zjištění verze DirectX
Prodáno 14.10.2014 na www.Kosmas.cz zákazníkovi [email protected], číslo objednávky 591522, UID: 7e15c933-e4b8-4e2a-94a6-c3fd2dffdec9
28
DIRECTX
2. Základní aplikace
Prodáno 14.10.2014 na www.Kosmas.cz zákazníkovi [email protected], číslo objednávky 591522, UID: 7e15c933-e4b8-4e2a-94a6-c3fd2dffdec9
DIRECTX
29
3. DirectDraw
3. DirectDraw Jako první součást DirectX si popíšeme DirectDraw. Základní údaje jsme si již uvedli v přehledu komponent DirectX. Řekli jsme, že jde o 2D grafické rozhraní, které má množství výhod. Kromě přímého přístupu do paměti grafické karty umožňuje rychle a efektivně pracovat se surfaces (povrchy, tedy oblasti paměti, kde jsou uložena grafická data). DirectDraw nabízí funkce pro operace s těmito surfaces, jako jejich přesouvání, překrývání nebo ořezávání. V dnešní době již o DirectDraw příliš neuslyšíte. Místo něj se používá Direct3D, které dokáže pracovat s 2D i 3D grafikou. Nicméně si myslím, že přesto stojí DirectDraw za pozornost. Jednak práce s ním je jednodušší než s Direct3D, a pokud se naučíte porozumět rozhraní DirectDraw a programovat s ním, případný přechod na Direct3D bude snazší. Výhodou je i skutečnost, že nemusíte mít nainstalované DirectX9, ale stačí verze 7 nebo 8. Takové aplikace můžete vytvářet i pro Windows 95. Než se pustíme do programování, uveďme si několik důležitých poznámek. První informace se týká způsobu integrace DirectDraw do systému. Blokové schéma je uvedené na obrázku 3.1. Z něj je patrný způsob komunikace Win32 aplikací s grafickou kartou. Pokud nepoužíváme DirectDraw, tato komunikace probíhá přes GDI (Graphics Device
3. DirectDraw
Prodáno 14.10.2014 na www.Kosmas.cz zákazníkovi [email protected], číslo objednávky 591522, UID: 7e15c933-e4b8-4e2a-94a6-c3fd2dffdec9
30
DIRECTX
Win32 Application
DirectDraw
HAL
HEL
GDI
DDI
Hardware (Video Card) Obrázek 3.1: Integrace DirectDraw do systému
Interface) a DDI (Display Device Interface). GDI je grafické rozhraní (součást operačního systému Windows), které poskytuje aplikacím nezávislé rozhraní k obrazovce a tiskárně. Jeho výhodou je, že se programátor nemusí starat, jaké hardwarové zařízení je v konkrétním počítači. Správně napsané aplikace tak fungují stejně na rozdílných technicky vybavených počítačích. Nevýhodou je ovšem pomalost, proto není tento způsob komunikace především u graficky náročnějších aplikací vhodný. Alternativou je v takových případech využití DirectDraw. DirectDraw urychluje grafické operace tím, že se snaží v maximální míře využívat HAL (Hardware Abstraction Layer). Tato zkratka představuje rozhraní specifické pro každou grafickou kartu (často bývá součástí příslušného ovladače adaptéru). V praxi to znamená, že každá funkce, která je hardwarově podporována grafickou kartou, se takto provede. Pokud příslušná funkce podporována není, bývá softwarově emulována pomocí HEL (Hardware Emulation Layer). V takovém případě je ovšem celý proces pomalejší a zpomalí se samozřejmě i běh celého programu. Pomocí metody IDirectDraw::GetCaps můžeme u každé funkce DirectDraw zjistit, zda je či není hardwarově podporována. Na základě těchto informací o používaných funkcích DirectDraw v programu se aplikace dá optimalizovat a můžeme nalézt vhodnou kombinaci kvality zobrazování aplikace a její rychlostí. Toto vše funguje v aplikacích, které běží v okně nebo na celé obrazovce. Další výhodou jsou tzv. page flipping a back buffering. Abychom si tyto pojmy osvětlili, je třeba nejprve vědět, co je to buffer. V případě DirectDraw jde o určitou oblast paměti, která je přidělena pro ukládání obrazových dat. V aplikacích DirectDraw bývá bufferů hned několik. Zřejmě nejdůležitější je tzv. front buffer. V něm je uloženo to, co vidíme na obrazovce, čili obrazová data. Dále bývá v aplikacích alespoň jeden back buffer. Zde se ukládají data, která se mají na obrazovku teprve vykreslit. Funguje to zpravidla tak, že zatímco na obrazovce počítače vidíme obsah front bufferu, do back bufferu vykreslujeme to, co má být na obrazovce v příštím okamžiku. Potom se obsah back bufferu do front bufferu přenese (to provádí metoda IDirectSurface7::Blt, proto se této operaci také
3. DirectDraw
Prodáno 14.10.2014 na www.Kosmas.cz zákazníkovi [email protected], číslo objednávky 591522, UID: 7e15c933-e4b8-4e2a-94a6-c3fd2dffdec9
DIRECTX
31
říká blittting), nebo se jejich obsahy zamění (tomuto procesu se říká flipping – odtud název page flipping na začátku tohoto odstavce). Pokud nemáme žádný back buffer (existuje pouze front buffer), procesu se říká single buffering. Tento způsob má výhodu v úspoře paměti, ale prakticky se nepoužívá, neboť v takovém případě vykreslujeme přímo do front bufferu a obraz problikává. Nejčastěji se používá double buffering, kdy máme jeden front buffer a jeden back buffer. Někdy bývá back bufferů i více, v případě jednoho front bufferu a dvou back bufferů se tomuto procesu říká triple buffering. V takovém případě máme sice větší nároky na paměť, ale můžeme si dopředu připravit více obrazových informací. Na závěr teoretického povídání o DirectDraw si ještě povězme o nejpoužívanějších rozhraních, která se v tomto API vyskytují. Jejich přehled je uveden v tabulce 3.1. Tabulka 3.1: Rozhraní DirectDraw
Popis Základní stavební kamen každé DirectDraw aplikace, s vývojem se měnily i verze rozhraní: IDirectDraw, IDirectDraw2, IDirectDraw4, nejnovější je IDirectDraw7.
IDirectDrawSurface
Rozhraní pro objekty reprezentující úsek paměti, kde jsou uloženy grafické informace – existuje několik verzí, IDirectDrawSurface, IDirectDrawSurface2, IDirectDrawSurface3, IDirectDrawSurface4 a nejnovější IDirectDrawSurface7.
IDirectDrawPalette
Rozhraní pro objekty uchovávající palety barev (používá se v případě grafických prvků s barevnou hloubkou menší nebo rovno 256 barev).
IDirectDrawClipper
Rozhraní pro objekty, které kontrolují přenos obrazových dat a zabraňují tak vykreslování mimo obrazovku.
3. DirectDraw
Rozhraní IDirectDraw
Tolik alespoň ve stručnosti k základním informacím o DirectDraw. Výše uvedené skutečnosti vám pomohou při pochopení funkčnosti DirectDraw aplikací. V této chvíli již máme k dispozici dostatek informací a můžeme začít vytvářet první DirectDraw programy.
3.1 Jednoduchá aplikace s DirectDraw Naše první DirectDraw aplikace bude běžet v celoobrazovkovém režimu a bude střídavě zobrazovat obsahy front a back bufferu. Pro jednoduchost a jejich rozlišení do každého z nich vypíšeme text, který nám je jednoznačně rozliší. Vyjdeme z Win32 aplikace, kterou jsme vytvořili ve druhé kapitole. Tu si pochopitelně náležitě upravíme. Části programu, které zůstanou stejné, tu již uvádět nebudeme. Zaměřím se tak na popis nových informací. Stejně jako v čisté Win32 aplikaci budeme mít šest souborů. Pro zjednodušení budou ponechány i jejich názvy. Začneme souborem Global.h, který – jak víme – obsahuje globální vlastnosti celé aplikace. Ten bude nyní vypadat následovně: #define _CRT_SECURE_NO_DEPRECATE 1 #pragma once
3.1 Jednoduchá aplikace s DirectDraw
Prodáno 14.10.2014 na www.Kosmas.cz zákazníkovi [email protected], číslo objednávky 591522, UID: 7e15c933-e4b8-4e2a-94a6-c3fd2dffdec9
32
DIRECTX
#include <windows.h> #include <stdio.h> #include #pragma comment (lib,"dxguid.lib") #pragma comment (lib,"ddraw.lib") #define APP_NAME
TEXT("DirectDraw aplikace")
#define SCREEN_WIDTH #define SCREEN_HEIGHT #define SCREEN_DEPTH
640 480 8
#define WAITING_FOR_FLIP #define FRONTTEXT #define BACKTEXT
1000 TEXT(" Front buffer ") TEXT(" Back buffer ")
#define RELEASE_DX_OBJECT(p)
{if(p){(p)->Release(); p = NULL;};};
extern TCHAR g_tcError[256]; Na začátku máme vloženou symbolickou konstantu _CRT_SECURE_NO_DEPRECATE, s hodnotou 1. Budete-li používat kompilátor Visual C++ 6.0, tak se tímto makrem nemusíte vůbec zabývat a můžete ho smazat nebo vložit do komentáře. Toto makro je vhodné implementovat do programů, které budete kompilovat ve Visual C++ Express Edition, kde potlačuje varovné hlášky kompilátoru kvůli funkci sprintf. Tuto funkci budeme používat téměř v každém programu pro zápis případných chyb, které mohou vzniknout při běhu programu. Podmínkou potlačení varovných hlášení je, aby byla tato symbolická konstanta definována hned na začátku souboru, ještě před direktivou #pragma once. Na začátku vkládáme nejen hlavičkový soubor windows.h, ale i ddraw.h. Tento soubor budeme vkládat v každé aplikaci využívající DirectDraw. Obsahuje vše potřebné, co k tvorbě takových aplikací potřebujeme. Je standardní součástí DirectX SDK a v kompilátoru je třeba k němu mít nastavenou cestu (viz podkapitolu 2.1). Dále následují dvě direktivy #pragma. Ty nám říkají, jaké knihovny budou k našemu projektu přilinkovány. Knihovna dxguid.lib poskytuje základní objekty a funkce společné pro různá DirectX rozhraní, ddraw.lib navíc vše z DirectDraw. Vložen je i hlavičkový soubor stdio.h. Důvodem je funkce sprintf, kterou budeme používat v našem programu pro ukládání chybových hlášení (viz dále). Jak víme, symbolická konstanta APP_NAME udává jméno naší aplikace. Dále následuje trojice maker SCREEN_WIDTH, SCREEN_HEIGHT a SCREEN_DEPTH. Jak z jejich názvu vyplývá, první dvě z nich udávají rozlišené obrazovky. To máme nastaveno na jeden ze standardních režimů – 640 × 480 pixelů (toto rozlišení si můžete změnou příslušných hodnot nastavit i jinak – například 800 × 600). Poslední makro udává barevnou hloubku. Číslo u této symbolické konstanty tedy představuje počet bitů pro každou jednotlivou barvu. Jinými slovy, 8bitová barevná hloubka nám říká, že se bude používat 28, tedy 256 barev. Makro WAITING_FOR_FLIP udává hodnotu v milisekundách, jak se budou oba buffery zaměňovat. Hodnota 1000 znamená, že tato změna bude provedena každou sekundu. Rozlišení front a back bufferu poznáme podle toho, že do každého z nich zapíšeme příslušný text. Tyto texty máme uloženy v symbolických konstantách FRONTTEXT a BACKTEXT.
3. DirectDraw
Prodáno 14.10.2014 na www.Kosmas.cz zákazníkovi [email protected], číslo objednávky 591522, UID: 7e15c933-e4b8-4e2a-94a6-c3fd2dffdec9
DIRECTX
33
Předposlední položkou v souboru Global.h je makro RELEASE_DX_OBJECT s jedním parametrem. Tímto parametrem je očekáván objekt. Metoda Release provádí uvolňování paměti. Tato metoda je součástí rozhraní IUnkown, které je společné pro všechna rozhraní DirectDraw. Po dealokaci paměti touto metodou se ještě provádí uvolnění příslušných ukazatelů příkazem p = NULL. Volání tohoto makra se u objektů DirectDraw provádí v okamžiku požadavku konce jejich platnosti, v našem případě v okamžiku ukončení programu. Jakým způsobem se to provádí a pro které naše objekty se toto makro bude volat, uvidíme dále. Na konci souboru Global.h máme uvedenou deklaraci pole znaků s názvem g_tcError. V objektovém programování bychom se globálním deklaracím měli vyhýbat. K tomuto poli znaků ovšem potřebujeme přístup z různých programových modulů. Sem se bude ukládat text chyby, pokud při nějaké operaci vznikne. Konkrétně jde o chyby vznikající při inicializaci celé aplikace nebo při chybném provedení některých operací. Deklaraci máme jako extern – skutečná deklarace i s počátečním naplněním se nachází v souboru WinMain.cpp, který si popíšeme nyní.
while(true) { if(PeekMessage(&msg, NULL, 0, 0, PM_REMOVE)) { if(msg.message == WM_QUIT) break; TranslateMessage(&msg); DispatchMessage(&msg); } if(b_ActiveApp) App.MainLoop(); }
3. DirectDraw
Rozdíly proti předchozí verzi programu v souboru WinMain.cpp jsou následující. Zavedli jsme novou globální proměnnou unsigned int b_ActiveApp = TRUE;. Do této proměnné budeme ukládat informace o tom, zda je naše okno aplikace aktivní. Na začátku aplikace aktivní je, proto je tato proměnná inicializována na TRUE. Důvodem použití této proměnné je, že pokud aplikace aktivní nebude, nebude probíhat algoritmus našeho programu, nebude se tedy zbytečně zatěžovat procesor. Toto se dá velice jednoduše ošetřit v nekonečném cyklu při zpracovávání zpráv, což je další úprava v souboru WinMain.cpp:
Jak z tohoto výpisu vidíme, máme zde následující podmínku: pokud bude mít proměnná b_ActiveApp nenulovou hodnotu, zavolá se metoda MainLoop instance App, která zajišťuje běh hlavní smyčky naší aplikace (k těle této metody se dostaneme dále). Poslední globální deklaraci, kterou provádíme v našem zkušebním souboru WinMain.cpp, je TCHAR g_tcError[256];. Jde o zmíněné pole znaků, do něhož se v případě chyby bude ukládat textový řetězec, který bude chybu popisovat. Preventivně do tohoto pole na začátku hlavní smyčky programu ukládáme textový řetězec "OK" příkazem sprintf(g_tcError,TEXT("OK"));. Pokud však při inicializaci k nějaké chybě přece jen dojde, program se hned ukončí. To se provede tak, že se zavolá App.Terminate() a zobrazí se dialogové okno s textem chyby: if(!App.Initialize()) { App.Terminate(); MessageBox (NULL, g_tcError, APP_NAME, MB_ICONERROR); return (0);
3.1 Jednoduchá aplikace s DirectDraw
Prodáno 14.10.2014 na www.Kosmas.cz zákazníkovi [email protected], číslo objednávky 591522, UID: 7e15c933-e4b8-4e2a-94a6-c3fd2dffdec9
34
DIRECTX
} App.WriteText(); Když se zmiňujeme o volání metody App.Terminate() – tato metoda se volá i při ukončení programu, tedy bude to předposlední příkaz hlavní smyčky programu před return (msg.wParam);. Jejím cílem v tomto programu bude provést uvolnění paměti od všech vytvořených objektů DirectDraw. K bližšímu popisu této metody se ještě vrátíme v dalších odstavcích této kapitoly. Po úspěšně provedené inicializaci se u instance App volá metoda WriteText, která zajistí vypsání textů do front a back bufferů (viz dále). Další změnou proti čisté Win32 aplikaci je volání metody Terminate instance App ve chvíli, kdy program regulérně, tedy na žádost uživatele končí. Uvedli jsme, že se v této metodě provádí uvolnění paměti od vytvořených objektů DirectDraw. K tomu se ale také ještě dostaneme. Poslední úprava v souboru WinMain.cpp se týká funkce zpracovávající zprávy Windows. Proti původnímu programu je tato funkce – tentokrát z důvodu přehlednosti a snadnější správy – přesunuta do souboru WinMain.cpp. Funkce vypadá nyní následovně: LRESULT CALLBACK WndProc(HWND m_hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam) { switch(uMsg) { case WM_ACTIVATEAPP: b_ActiveApp = wParam; return (0); case WM_KEYDOWN: switch(wParam) { case VK_ESCAPE: PostQuitMessage(0); return (0); } break; case WM_DESTROY: PostQuitMessage(0); return (0); } return DefWindowProc(m_hWnd, uMsg, wParam, lParam); } Ve funkci je pouze jediná změna: máme navíc zpracování zprávy WM_ACTIVATEAPP. Tato zpráva se generuje pokaždé, když okno aplikace přejde z aktivního stavu do neaktivního nebo naopak. V parametru wParam pak můžeme zjistit aktuální stav a tento stav ukládáme do naší proměnné b_ActiveApp. V souboru CApplication.h máme deklarovanou třídu CApplication. Té přibyly dvě zmíněné metody: MainLoop(void) a WriteText(void). Celkově vypadá třída CApplication následovně:
3. DirectDraw
Prodáno 14.10.2014 na www.Kosmas.cz zákazníkovi [email protected], číslo objednávky 591522, UID: 7e15c933-e4b8-4e2a-94a6-c3fd2dffdec9
DIRECTX
35
#pragma once #include "CWindow.h" class CApplication { private: CWindow m_Window; long m_lPrevTick, m_lCurTick; public: CApplication(void); ~CApplication(void); bool Initialize(void); void Terminate(void); void MainLoop(void); void WriteText(void);
Ve výpisu si také můžeme všimnout dvou nových soukromých atributů: m_lPrevTick a m_lCurTick. Do těchto proměnných se bude ukládat počet milisekund od spuštění Windows. Proč? Vzpomeňme na soubor Global.h a na makro WAITING_FOR_FLIP. Tím, že budeme vědět, kolik „tiků“ bylo dříve a kolik nyní, můžeme poznat, kolik času uplynulo, a provádět tak operace v časově určených okamžicích. Jak to vypadá konkrétně, zjistíme pohledem do souboru CApplication.cpp. Protože je v něm ale změn více, uvedeme si tento soubor celý:
3. DirectDraw
};
#pragma once #include "Global.h" #include "CApplication.h" CApplication::CApplication(void) { m_lPrevTick=0; m_lCurTick=GetTickCount(); } CApplication::~CApplication(void) { m_Window.Terminate(); } bool CApplication::Initialize(void) { if(!m_Window.Initialize()) return (false); return (true); }
3.1 Jednoduchá aplikace s DirectDraw
Prodáno 14.10.2014 na www.Kosmas.cz zákazníkovi [email protected], číslo objednávky 591522, UID: 7e15c933-e4b8-4e2a-94a6-c3fd2dffdec9
36
DIRECTX
void CApplication::Terminate(void) { m_Window.Terminate(); } void CApplication::MainLoop(void) { m_lCurTick = GetTickCount(); if(m_lCurTick-m_lPrevTick > WAITING_FOR_FLIP) { m_lPrevTick = m_lCurTick; m_Window.FlipFrame(); } } void CApplication::WriteText(void) { m_Window.WriteText(); } V konstruktoru této třídy nastavujeme proměnnou m_lPrevTick na hodnotu 0. Naproti tomu m_lCurTick naplníme hodnotou, kterou nám dá funkce GetTickCount. Tato funkce provádí přesně to, co jsme si napsali výše – vrací počet milisekund od spuštění Windows. Jako další si projděme metodu MainLoop. Zde běží smyčka programu a náš program má pouze přepínat dvě obrazovky (front a back buffer). Tuto činnost provádí metoda FilpFrame, kterou popíšeme níže. Nyní nás ale zajímá, kdy se volá. Vše nám udává podmínka if(m_lCurTick-m_lPrevTick > WAITING_FOR_FLIP). Protože makro WAITING_ FOR_FLIP máme nastaveno na hodnotu 1000 (milisekund, tedy jednu sekundu), metoda FlipFrame, která se bude starat o výměnu obrazovky, bude volána každou vteřinu. Ostatní metody neprovádí mnoho zajímavého. Destruktor může být prázdný, z důvodu bezpečnosti zde ale voláme metodu Terminate instance m_Window. Tělo metody Initialize() je stejné jako u čisté Win32 aplikace. Zbylé dvě metody Terminate a WriteText pouze volají stejnojmenné metody instance m_Window. Asi nejzajímavější obsah pro nás skrývá poslední dvojice souborů, tedy CWindow.h a CWindow.cpp. Prvně jmenovaný obsahuje deklaraci třídy, která vypadá takto: #pragma once class CWindow { private: HWND m_hWnd; LPDIRECTDRAW7 m_lpDD; LPDIRECTDRAWSURFACE7 m_lpDDFront, m_lpDDBack; RECT m_ddRect; public: CWindow(void); ~CWindow(void);
3. DirectDraw
Prodáno 14.10.2014 na www.Kosmas.cz zákazníkovi [email protected], číslo objednávky 591522, UID: 7e15c933-e4b8-4e2a-94a6-c3fd2dffdec9
DIRECTX
37
bool Initialize(void); void Terminate(void); void FlipFrame(void); void WriteText(void); friend LRESULT CALLBACK WndProc(HWND m_hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam); }; Vidíme, že nám přibylo několik atributů. Prvním z nich je m_lpDD, což bude dynamický (proto je to ukazatel) objekt rozhraní IDirectDraw7, o kterém jsme si již řekli, že je základem každé DirectDraw aplikace. Pokud vám to v tuto chvíli připadá matoucí, tak vězte, že příkazy: LPDIRECTDRAW7 m_lpDD; a
jsou identické. Kromě toho budeme mít v naší aplikaci po jednom front a back bufferu, proto zde máme vytvořené dva objekty m_lpDDFront a m_lpDDBack. Oba jsou typu IDirectDrawSurface. Sedmičky na konci říkají, že používáme nejnovější rozhraní 7. Poslední atribut je identifikátor m_ddRect datového typu RECT. Ve skutečnosti je RECT struktura definující obdélníkovou oblast. My v ní budeme uchovávat rozměry obrazovky, které budeme potřebovat k vypsání textu do front nebo back bufferu. Potom zde máme čtyři metody – Initialize, Terminate, FlipFrame a WriteText. Jejich těla, stejně jako těla ostatních členských metod třídy CWindow, jsou definována v souboru CWindow.cpp. Postupně si je uvedeme a okomentujeme. Začněme konstruktorem:
3. DirectDraw
IDirectDraw7 *m_lpDD;
CWindow::CWindow(void) { m_hWnd = NULL; m_lpDD = NULL; m_lpDDFront = NULL; m_lpDDBack = NULL; } Konstruktor obsahuje pouze počáteční inicializace většiny členských atributů třídy. Možná by se to mohlo zdát zbytečné, ale každý zkušenější programátor ví, že v programech je lepší myslet i na bezpečnost programového kódu a vyhnout se tak případným nepříjemným chybám. Při práci s těmito atributy v jiných metodách se takovým potížím vyhýbáme tak, že se před prací s těmito atributy přesvědčíme, zda neobsahují inicializační hodnotu. Pokud by ji obsahovaly, mohlo by to znamenat chybu, na kterou můžeme v programu přímo reagovat. Toto uvidíme v ostatních metodách třídy CWindow. Tělo destruktoru tvoří pouze volání metody CWindow::Terminate, proto si obě metody vypišme zároveň: CWindow::~CWindow(void) { Terminate(); }
3.1 Jednoduchá aplikace s DirectDraw
Prodáno 14.10.2014 na www.Kosmas.cz zákazníkovi [email protected], číslo objednávky 591522, UID: 7e15c933-e4b8-4e2a-94a6-c3fd2dffdec9
38
DIRECTX
void CWindow::Terminate(void) { RELEASE_DX_OBJECT(m_lpDDFront); RELEASE_DX_OBJECT(m_lpDD); CloseWindow(m_hWnd); } V metodě Terminate dvakrát voláme makro RELEASE_DX_OBJECT, které – jak víme – je definováno v souboru Global.h. Těmito příkazy rušíme objekty m_lpDDFront, m_lpDD a uvolňujeme paměť, která jim byla přidělena. Metoda Terminate se volá v okamžiku, kdy náš program končí (tuto metodu voláme i v proceduře zpracování zpráv Windows). Možná vás napadne otázka, proč nerušíme objekt back bufferu m_lpDDBack. Odpověď je jednoduchá. Front a back buffer budou tvořit komplexní surface (viz dále). Proto nemusíme provádět uvolnění těchto objektů zvlášť a stačí tedy zrušit objekt m_lpDDFront. Nakonec ještě příkazem CloseWindow zavíráme okno aplikace. Toto je potřeba především v případech, kdy program nečekaně skončil (například chyba při inicializaci), aby se na obrazovce mohlo objevit informační okno s textem vzniklé chyby. Metoda CWindow::Initialize je ze všech metod našeho programu nejrozsáhlejší. Vytvoří okno aplikace, inicializuje DirectDraw a všechny potřebné objekty. Její tělo vypadá následovně: bool CWindow::Initialize(void) { WNDCLASS wc; DDSURFACEDESC2 ddsd; DDSCAPS2 ddscaps; ZeroMemory(&wc, sizeof(wc)); wc.style = CS_HREDRAW|CS_VREDRAW; wc.lpfnWndProc = WndProc; wc.hInstance = GetModuleHandle(NULL); wc.hIcon = LoadIcon(NULL, IDI_WINLOGO); wc.hCursor = LoadCursor(NULL, IDC_ARROW); wc.hbrBackground = (HBRUSH) GetStockObject(BLACK_BRUSH); wc.lpszClassName = APP_NAME; if(!RegisterClass(&wc)) { sprintf(g_tcError,TEXT("Chyba při registraci okna aplikace!")); return (false); } if(!(m_hWnd = CreateWindowEx(0, APP_NAME, APP_NAME, WS_POPUP, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, NULL, NULL, wc.hInstance, NULL))) { sprintf(g_tcError,TEXT("Chyba při vytváření okna aplikace!")); return (false); } if((DirectDrawCreateEx(NULL, (void**)&m_lpDD, IID_IDirectDraw7, NULL))!=DD_OK)
3. DirectDraw
Prodáno 14.10.2014 na www.Kosmas.cz zákazníkovi [email protected], číslo objednávky 591522, UID: 7e15c933-e4b8-4e2a-94a6-c3fd2dffdec9
DIRECTX
39
{ sprintf(g_tcError,TEXT("Nelze vytvořit objekt DirectDraw!")); return (false); } if((m_lpDD->SetCooperativeLevel(m_hWnd, DDSCL_ALLOWREBOOT | DDSCL_EXCLUSIVE | DDSCL_FULLSCREEN)) != DD_OK) { sprintf(g_tcError,TEXT("Nelze nastavit vlastnosti DirectDraw aplikace!")); return (false); } if((m_lpDD->SetDisplayMode(SCREEN_WIDTH, SCREEN_HEIGHT, SCREEN_DEPTH, 0, 0)) != DD_OK) { sprintf(g_tcError,TEXT("Nelze nastavit rozlišení obrazovky!")); return (false); }
ZeroMemory(&ddsd, sizeof(ddsd)); ddsd.dwSize = sizeof(ddsd); ddsd.dwFlags = DDSD_CAPS | DDSD_BACKBUFFERCOUNT; ddsd.ddsCaps.dwCaps = DDSCAPS_PRIMARYSURFACE | DDSCAPS_FLIP | DDSCAPS_COMPLEX; ddsd.dwBackBufferCount = 1; if((m_lpDD->CreateSurface(&ddsd, &m_lpDDFront, NULL)) != DD_OK) { sprintf(g_tcError,TEXT("Nelze vytvořit front buffer!")); return (false); }
3. DirectDraw
GetClientRect(m_hWnd, &m_ddRect);
ZeroMemory(&ddscaps, sizeof(ddscaps)); ddscaps.dwCaps = DDSCAPS_BACKBUFFER; if((m_lpDDFront->GetAttachedSurface(&ddscaps, &m_lpDDBack)) != DD_OK) { sprintf(g_tcError,TEXT("Nelze vytvořit back buffer!")); return (false); } ShowWindow(m_hWnd, SW_SHOW); UpdateWindow(m_hWnd); return (true); } Nejprve se na celou metodu zkusme podívat více s nadhledem. Téměř všechny inicializační příkazy testujeme na návratovou hodnotu. Pokud něco neproběhlo v pořádku, příkazem sprintf uložíme text chyby do textového pole g_tcError a celá inicializační
3.1 Jednoduchá aplikace s DirectDraw
Prodáno 14.10.2014 na www.Kosmas.cz zákazníkovi [email protected], číslo objednávky 591522, UID: 7e15c933-e4b8-4e2a-94a6-c3fd2dffdec9
40
DIRECTX
metoda končí neúspěchem, což poznáme podle vrácené hodnoty false. V celé knize budeme pro jednoduchost ošetřovat chyby tímto způsobem. Jednotlivé metody různých DirectX rozhraní ovšem vrací hodnotu typu HRESULT (odpovídá typu proměnné unsigned long), ze které se dá kód chyby rovněž vyčíst. Jak v našem kódu vidíme, v případě, že je vše v pořádku, metody rozhraní iDirectDraw vrací hodnotu DD_OK. Ve všech ostatních případech se vrací jiné hodnoty, které by měly usnadnit zjištění, k jaké chybě došlo a proč. Pokud byste chtěli ošetřovat chyby tímto způsobem, nahlédněte do dokumentace k DirectX SDK. Nyní si metodu CWindow::Initialize popišme podrobněji. Prozatím se vyhněme novým datovým typům ddsd a ddscaps. Ty patří k DirectDraw. Zmíníme se o nich až u příkazů, které je využívají. Začátek inicializace okna aplikace je téměř stejný jako u čisté Win32 aplikace. Tedy nejprve naplníme strukturu wc (typ WNDCLASS), kterou zaregistrujeme, a vytvoříme okno zavoláním funkce CreateWindowEx. Proti původní verzi si můžeme všimnout pouhých dvou rozdílů. První z nich je, že okno vyplníme černou barvou (dříve jsme měli bílou). To je položka hbrBackground struktury wc. Druhým rozdílem je čtvrtý parametr funkce CreateWindowEx. Dříve jsme zde měli WS_OVERLAPPEDWINDOW, nyní je tam WS_POPUP. Tento parametr udává styl okna. Hodnota WS_OVERLAPPEDWINDOW znamenala, že okno programu bude klasické okno Windows se záhlavím a v tomto záhlaví budou tlačítka pro minimalizaci, maximalizaci a zavření okna. Naše aplikace ale bude běžet v celoobrazovkovém režimu. Parametr WS_POPUP říká, že bude vytvořeno pouze překryvné okno. Jakmile okno aplikace vytvoříme, nastává správný čas pro vytvoření objektů DirectDraw a front a back buffery. První funkci, kterou v metodě CWindow::Initialize máme a která se týká DirectDraw, je DirectDrawCreateEx. Tato funkce vytváří objekt m_lpDD (objekt rozhraní IDirectDraw7). Má celkem čtyři parametry. První z nich představuje hodnotu reprezentující ovladač grafické karty. Máme zde hodnotu NULL, což znamená, že se použije aktuální ovladač. Kromě tohoto zde mohu být ještě i hodnoty DDCREATE_EMULATIONONLY, což znamená, že se u všech operací DirectDraw bude vždy používat softwarová emulace, a DDCREATE_HARDWAREONLY, což znamená opak. Bude se tedy pouze využívat hardwarové vybavení grafické karty a operace, které nebudou podporovány, budou vracet chybový kód DDERR_UNSUPPORTED. Druhým parametrem je ukazatel na objekt DirectDraw, který chceme vytvořit, tedy v našem případě m_lpDD. Třetí parametr je ukazatel na použité COM rozhraní. Nejnovější rozhraní IDirectDraw7 má identifikátor IID_IDirectDraw7. Poslední parametr byl míněn pro začlenění technologií COM rozhraní. Nicméně se nepoužívá, a proto tam vždy bývá hodnota NULL. Poté, co je objekt typu DirectDraw úspěšně vytvořen, definujeme vlastnosti naší aplikace. Toto nastavení provádí metoda IDirectDraw7::SetCooperativeLevel. Tato metoda má dva parametry. První je handle okna aplikace, které se tyto vlastnosti týkají, druhým parametrem jsou vlastnosti, jež chceme nastavit. My tu máme DDSCL_ALLOWREBOOT, což znamená, že je povolena klávesová zkratka CTRL+ALT+DEL, tedy že po jejím stisknutí se dostaneme do okna Správce úloh ve Windows, odkud můžeme aplikaci ukončit nebo se přepnout do jiné aplikace. Dále tu máme příznaky DDSCL_EXCLUSIVE a DDSCL_FULLSCREEN, které znamenají, že aplikace bude běžet v celoobrazovkovém režimu a že aplikace má výhradní právo (tyto příznaky musí být použity společně). Následuje metoda IDirectDraw7::SetDisplayMode. Tato metoda provádí nastavení rozlišení obrazovky a barevné hloubky. První parametr představuje šířku a druhý výšku obrazovky (obě hodnoty jsou v pixelech). My k tomu používáme makra SCREEN_WIDTH a SCREEN_HEIGHT. Stejně tak používáme i makro SCREEN_DEPTH pro nastavení barevné hloubky (třetí parametr). Číslo udává počet bitů na pixel. Čtvrtý parametr udává obnovovací frekvenci obrazu monitoru. Protože ne každý monitor musí zvládnout nastavenou
3. DirectDraw
Prodáno 14.10.2014 na www.Kosmas.cz zákazníkovi [email protected], číslo objednávky 591522, UID: 7e15c933-e4b8-4e2a-94a6-c3fd2dffdec9
DIRECTX
41
hodnotu, je lepší uvádět hodnotu 0, což znamená, že se použije výchozí nastavení. Poslední parametr byl navržen pro použití v nestandardním režimu rozlišení tzv. ModeX, který se ale zpravidla nepoužívá (a nepoužíváme ho ani my), proto je tu hodnota 0. Objekt rozhraní IDirectDraw7 máme vytvořen a stejně tak máme nastaveny vlastnosti naší aplikace. Následuje vytvoření front a back bufferu. Ještě předtím však voláme funkci Win32 GetClientRect. Tato funkce se používá pro zjištění velikosti klientské části okna. Příslušné okno je dáno svým handle (první parametr funkce) a zjištěné rozměry se ukládají do struktury typu RECT (druhý parametr funkce).
Následuje vytvoření front bufferu metodou rozhraní IDirectDraw7::CreateSurface. Jejím prvním parametrem je reference na strukturu typu DDSURFACEDESC2, ve které jsou uloženy vlastnosti vytvářeného bufferu (v našem případě je to ddsd). Potom následuje reference na jméno objektu surface, který ho bude reprezentovat (tedy m_lpDDFront). Poslední parametr byl opět zamýšlen pro agregaci COM rozhraní, což však nakonec nebylo implementováno, proto se zde vždy zapisuje hodnota NULL.
3. DirectDraw
Vytvářený surface má vlastnosti, které jsou uloženy v instanci struktury DDSURFACEDESC2. Tuto instanci jsme v programu pojmenovali ddsd. Aby neobsahovala nechtěné údaje, nejprve se celý její obsah vymaže globální funkcí ZeroMemory a teprve poté do ní uložíme potřebné údaje. Jde o velikost této instance (položka dwSize) a další naplněnou položkou jsou příznaky (položka dwFlags). Dva uvedené příznaky (DDSD_CAPS a DDSD_BACKBUFFERCOUNT) udávají, jaké další položky budou nastaveny. První z nich je ddsCaps.dwCaps. Zde používáme trojici příznaků. První je DDSCAPS_PRIMARYSURFACE, což znamená, že budeme vytvářet front buffer. Druhý příznak DDSCAPS_FLIP udává, že kromě front bufferu bude vytvořen také nejméně jeden back buffer. Poslední příznak DDSCAPS_COMPLEX se nastavuje, pokud pracujeme s více než s jedním surface (front bufferem) a další vytvořené surfaces budou s front bufferem propojeny. V našem příkladu bude tímto způsobem k front bufferu připojen back buffer. Poslední nastavenou položkou struktury ddsd je dwBackBufferCount, do níž se ukládá počet back bufferů. My budeme mít pouze jeden.
V případě úspěšného vytvoření front bufferu ještě vytvoříme back buffer. Zde namísto struktury DDSURFACEDESC2 používáme instanci struktury DDSCAPS2 (tuto instanci jsme nazvali ddscaps). Obsah této instance nejprve funkcí ZeroMemory vymažeme a poté nastavíme pouze položku dwCaps na příznak DDSCAPS_BACKBUFFER. Ten udává, že vytvářený surface bude back buffer. Protože je back buffer přiřazen front bufferu, pro jeho vytvoření se nepoužívá metoda IDirectDraw7::CreateSurface, ale IDirectDrawSurface7:: GetAttachedSurface. Metoda IDirectDrawSurface7::GetAttachedSurface má dva parametry – referenci na strukturu typu DDSCAPS2, ve které jsou uloženy vlastnosti vytvářeného surface, a dále referenci na objekt surface, který bude back buffer reprezentovat (tedy m_lpDDBack). Poslední dvě funkce, které v metodě Initialize nalezneme, jsou ShowWindow a UpdateWindow, které zajistí zobrazení a aktualizaci obrazovky aplikace. Zbývá nám projít poslední dvě metody. Nejprve se podívejme na WriteText, která se volá po inicializaci aplikace ještě předtím, než začneme zpracovávat zprávy Windows. Zapisuje do front i back bufferu texty, abychom je na obrazovce rozeznali. Tělo metody WriteText je následující: void CWindow::WriteText(void) { HDC hdc; if(m_lpDDFront->GetDC(&hdc) == DD_OK) { SetBkColor(hdc, RGB(0, 0, 255));
3.1 Jednoduchá aplikace s DirectDraw
Prodáno 14.10.2014 na www.Kosmas.cz zákazníkovi [email protected], číslo objednávky 591522, UID: 7e15c933-e4b8-4e2a-94a6-c3fd2dffdec9
42
DIRECTX
SetTextColor(hdc, RGB(255, 255, 0)); DrawText(hdc, FRONTTEXT, -1, &m_ddRect, DT_SINGLELINE | DT_CENTER | DT_VCENTER); m_lpDDFront->ReleaseDC(hdc); } if(m_lpDDBack->GetDC(&hdc) == DD_OK) { SetBkColor(hdc, RGB(255, 0, 0)); SetTextColor(hdc, RGB(0, 255, 0)); DrawText(hdc, BACKTEXT, -1, &m_ddRect, DT_SINGLELINE | DT_CENTER | DT_VCENTER); m_lpDDBack->ReleaseDC(hdc); } } Metoda obsahuje dva podmíněné bloky pro zápis do každého z obou bufferů – těla těchto podmínek jsou podobná. V podmínce, při jejímž splnění text vypisujeme, voláme metodu GetDC příslušného surface. Jak z názvu této metody vyplývá, tato metoda získává DC, přesněji řečeno HDC. HDC (Handle Device Context) je handle kontextu zařízení, které je potřeba pro zobrazení textu a grafiky v okně aplikace. Jeho hodnota se uloží do parametru této metody, tedy do proměnné hdc (typ HDC – jde o 32bitové celé číslo bez znaménka). Po ukončení výpisu do surface je nezbytné toto handle uvolnit, což provádí metoda ReleaseDC na konci příslušných bloků. To, co je uvnitř obou podmíněných bloků, není záležitostí DirectDraw, ale Win32API. Funkce SetBkColor nastavuje barvu pozadí textu, který budeme vypisovat. Prvním parametrem je kontext zařízení, jehož se toto nastavení týká, druhým je barevný kód, který zadáváme v barevném modelu RGB (red-green-blue). Stejné parametry má i funkce SetTextColor. Pomocí této funkce nastavujeme barvu písma. U front bufferu máme tedy nastavenou barvu textu žlutou a pozadí modré, u back bufferu zelené písmo na červeném pozadí. Výpis textu provádí funkce DrawText. Při pohledu na její parametry si všimněme jedné zajímavosti. Na první pohled mezi parametry není uvedeno, do jakého bufferu (front nebo back) zapisujeme. Opak je ale pravdou – hned první parametr je hdc, které jsme z každého bufferu získali, a samozřejmě má pro každý buffer jinou hodnotu. Druhým parametrem je textový řetězec, který vypisujeme. My zde používáme makra, jež jsou definována v souboru Global.h. Třetí parametr udává počet znaků textového řetězce, které se mají vypsat. Námi uvedená hodnota -1 říká, že za tento počet znaků se vezme počet znaků textu zadaným předchozím parametrem (řetězec musí být ukončen nulovým znakem). Další parametr je reference na strukturu typu RECT udávající oblast, ve které bude text vypisován a formátován. V našem případě je touto oblastí celá obrazovka. Posledním (pátým) příznakem je kombinace příznaků udávající formátování textu. Trojice příznaků DT_SINGLELINE, DT_CENTER a DT_VCENTER znamenají, že text bude vypsán na jeden řádek (i kdyby v něm byl znak pro odřádkování) a bude umístěn doprostřed obrazovky (na výšku i na šířku). Poslední dosud nepopsanou metodou našeho programu je FlipFrame. Tělo této metody je velice krátké: void CWindow::FlipFrame(void) { if(m_lpDDFront) m_lpDDFront->Flip(NULL, DDFLIP_WAIT); }
3. DirectDraw
Prodáno 14.10.2014 na www.Kosmas.cz zákazníkovi [email protected], číslo objednávky 591522, UID: 7e15c933-e4b8-4e2a-94a6-c3fd2dffdec9
DIRECTX
43
U existujícího objektu m_lpDDFront voláme členskou metodu Flip. Metoda IDirectDrawSurface7:: Flip provádí záměnu obsahu objektu rozhraní IDirectDrawSurface7, u kterého je volána s obsahem DirectDrawSurface, který je dán prvním parametrem této metody. Pokud uvedeme tento parametr jako hodnotu NULL, vezme se surface, který je s předchozím asociován. V našem příkazu tedy provede záměnu front a back bufferu. Z důvodu urychlení se ale častěji neprovádí přímé kopírování obsahů těchto surfaces, ale pouze se zamění ukazatele na ně. V obou případech se dosáhne stejného výsledku. Dojde k záměně obsahu obrazovky. Druhým parametrem metody flip jsou příznaky udávající vlastnosti záměny. Příznak DDFLIP_WAIT říká, že se záměna synchronizuje, tedy čeká se na vhodný okamžik zápisu na zobrazovací zařízení. Tím jsme dokončili popis celého programu. Pokud tento kód zkompilujete a spustíte, budou se vám v sekundových intervalech střídavě zobrazovat obsahy front a back bufferů.
3.2 Triple buffering
Popišme si opět všechny soubory. Jejich názvy zůstanou zachovány, žádné nové do našeho projektu nebudeme přidávat. Provedeme tedy změny přímo v našem původním programu. V souboru Global.h dojde pouze ke dvěma drobným změnám. Odstraníme symbolické konstanty FRONTTEXT a BACKTEXT a nahradíme je jinými: #define BACKTEXT1 TEXT(" Back buffer 1 ") #define BACKTEXT2 TEXT(" Back buffer 2 ")
3. DirectDraw
Aplikaci popsanou v předchozí kapitole nyní rozšíříme o několik novinek. Za prvé, budeme používat místo jednoho hned dva back buffery (triple buffering). Další změnou bude, že nebudeme zaměňovat obsahy front a back bufferů, ale obsahy obou back bufferů budeme střídavě přenášet do front bufferu. Výsledek bude tedy podobný jako u předchozí aplikace. Poslední změnou bude, že budeme obnovovat ztracené surfaces.
Jak vyplývá z jejich označení a obsahu, půjde o texty zapisované do dvou back bufferů. Soubor WinMain.cpp zůstane nezměněn, totéž platí i o souboru CApplication.h. Jedinou změnou v souboru CApplication.cpp je, že v těle metody MainLoop nevoláme metodu CWindow::FlipFrame, ale metodu CWindow::BltFast, kterou si nově vytvoříme. Metoda MainLoop tedy vypadá následovně: void CApplication::MainLoop(void) { lCurTick = GetTickCount(); if(m_lCurTick-m_lPrevTick >WAITING_FOR_FLIP) { m_lPrevTick = m_lCurTick; m_Window.BltFast(); } } Největší změny nastanou ve třídě CWindow a v definicích jejích metod. Tělo této třídy v souboru CWindow.h je následující: class CWindow { private: HWND m_hWnd; LPDIRECTDRAW7 m_lpDD;
3.2 Triple buffering
Prodáno 14.10.2014 na www.Kosmas.cz zákazníkovi [email protected], číslo objednávky 591522, UID: 7e15c933-e4b8-4e2a-94a6-c3fd2dffdec9
44
DIRECTX
LPDIRECTDRAWSURFACE7 m_lpDDFront, m_lpDDBack1, m_lpDDBack2; RECT m_ddRect; bool m_bDisplay; public: CWindow(void); ~CWindow(void); bool Initialize(void); void Terminate(void); void BltFast(void); void WriteText(void); void ClearScreen(DWORD dwColor = 0); friend LRESULT CALLBACK WndProc(HWND m_hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam); }; Mezi atributy této třídy nám přibude druhý back buffer (back buffery jsou pojmenovány m_lpDDBack1 a m_lpDDBack2). Dále přidáme binární hodnotu m_bDisplay. V ní budeme ukládat stavovou hodnotu, zda se má do front bufferu kopírovat první nebo druhý back buffer. Mezi metodami dojde k záměně – jak jsme uvedli výše, metodu FlipFrame nahradíme novou, která má název BltFast. Ještě přidáme novou metodu ClearScreen, která bude mazat obsahy obou back bufferů. Za pozornost stojí jediný parametr této metody, jenž udává barvu výplně (jeho implicitní hodnota je 0 – černá barva). Nyní se podívejme na definice jednotlivých metod. Tělo konstruktoru je obdobné jako v předchozím programu, tedy inicializujeme většinu členských atributů nulovými hodnotami: CWindow::CWindow(void) { m_hWnd = NULL; m_lpDD = NULL; m_lpDDFront = NULL; m_lpDDBack1 = NULL; m_lpDDBack2 = NULL; m_bDisplay = false; } Destruktor stejně jako u předešlého programu opět volá metodu Terminate, jejíž tělo se také nezměnilo. V metodě CWindow::Initialize dojde k více změnám, protože vytváříme druhý back buffer. Postup inicializace je ale stejný. Nejprve naplníme a zaregistrujeme okno aplikace – a vytvoříme ho. Potom vytvoříme objekt rozhraní IDirectDraw a nastavíme rozlišení a barevnou hloubku obrazovky aplikace. Následovat bude tvorba jednoho front a dvou back bufferů. Zde je kód, který tuto zmíněnou činnost provádí: ZeroMemory(&ddsd, sizeof(ddsd)); ddsd.dwSize = sizeof(ddsd); ddsd.dwFlags = DDSD_CAPS | DDSD_BACKBUFFERCOUNT;
3. DirectDraw
Prodáno 14.10.2014 na www.Kosmas.cz zákazníkovi [email protected], číslo objednávky 591522, UID: 7e15c933-e4b8-4e2a-94a6-c3fd2dffdec9
DIRECTX
45
ddsd.ddsCaps.dwCaps = DDSCAPS_PRIMARYSURFACE | DDSCAPS_FLIP | DDSCAPS_COMPLEX; ddsd.dwBackBufferCount = 2; if((m_lpDD->CreateSurface(&ddsd, &m_lpDDFront, NULL))!=DD_OK) { sprintf(g_tcError,TEXT("Nelze vytvořit front buffer!")); return (false); } ZeroMemory(&ddscaps, sizeof(ddscaps)); ddscaps.dwCaps = DDSCAPS_BACKBUFFER; if((m_lpDDFront->GetAttachedSurface(&ddscaps, &m_lpDDBack1)) != DD_OK) { sprintf(g_tcError,TEXT("Nelze vytvořit první back buffer!")); return (false); }
Oproti aplikaci z předchozí podkapitoly si můžeme všimnout hned několika rozdílů. První rozdíl je u položky dwBackBufferCount struktury ddsd, udávající počet back bufferů. Místo hodnoty 1 tu máme hodnotu 2. Vytvoření front bufferu a prvního back bufferu se jinak nemění. Ke změně ale dochází při tvorbě druhého back bufferu. Položku ddscaps. dwCaps nenaplňujeme příznakem DDSCAPS_BACKBUFFER, ale DDSCAPS_FLIP. Oba příznaky mají podobný význam, tento nový surface tedy bude také asociován k front bufferu. Rozdíl je ale v tom, že příznakem DDSCAPS_BACKBUFFER se označuje pouze první back buffer, druhý musí mít nastavený příznak DDSCAPS_FLIP.
3. DirectDraw
ZeroMemory(&ddscaps, sizeof(ddscaps)); ddscaps.dwCaps = DDSCAPS_FLIP; if((m_lpDDBack1->GetAttachedSurface(&ddscaps, &m_lpDDBack2)) != DD_OK) { sprintf(g_tcError,TEXT("Nelze vytvořit druhý back buffer!")); return (false); }
Pokud zaměříme pozornost na tělo metody CWindow::WriteText, zjistíme, že výpis textu je nezměněn, pouze místo do front bufferu budeme zapisovat do druhého back bufferu. Také na začátku voláme metodu CWindow::ClearScreen pro smazání obsahu obou back bufferů. Tělo metody CWindow::ClearScreen vypadá následovně: void CWindow::ClearScreen(DWORD dwColor) { DDBLTFX ddbltfx; ZeroMemory(&ddbltfx, sizeof(ddbltfx)); ddbltfx.dwSize = sizeof(ddbltfx); ddbltfx.dwFillColor = dwColor; m_lpDDBack1->Blt(NULL, NULL, NULL, DDBLT_COLORFILL | DDBLT_WAIT, &ddbltfx); m_lpDDBack2->Blt(NULL, NULL, NULL, DDBLT_COLORFILL | DDBLT_WAIT, &ddbltfx); }
3.2 Triple buffering
Prodáno 14.10.2014 na www.Kosmas.cz zákazníkovi [email protected], číslo objednávky 591522, UID: 7e15c933-e4b8-4e2a-94a6-c3fd2dffdec9
46
DIRECTX
Mazání obrazovky provádí metoda IDirectDrawSurface7::Blt. Tato metoda vlastně přenáší bloky dat mezi různými surfaces, nalezne proto využití i jinde. Jedním z parametrů této metody je struktura typu DDBLTFX, do které se ukládají údaje o prováděných rastrových operacích. Po vymazání obsahu instance této struktury (ddbltfx) a nastavení její velikosti (položka dwSize) již nastavujeme pouze dwFillColor, což je barva, kterou se má vyplňovat. Tato barva vstupuje do metody ClearScreen jako její jediný parametr. U tohoto parametru máme implicitně nastavenou hodnotu 0, což, jak víme, znamená černou barvu. Metoda IDirectDrawSurface7::Blt má celkem pět parametrů. Reference na strukturu typu DDBLTFX je až ten poslední. První argument je struktura typu RECT, udávající obdélníkovou oblast na cílovém surface, kam se má blok přenést. Hodnota NULL říká, že touto oblastí bude celý surface. Druhým parametrem je objekt rozhraní IDirectDrawSurface, odkud se má blok přenášet (nemáme žádný), a třetím je opět struktura typu RECT, která tentokrát udává obdélníkovou oblast zdrojového surface, jenž se má přenést (opět nezadáváme). Čtvrtým argumentem jsou příznaky přenosu. DDBLT_COLORFILL znamená, že se bude cílová oblast vyplňovat jedinou barvou danou strukturou typu DDBLTFX a DDBLT_WAIT značí, že se nemusí přenášet blok dat okamžitě (při přenosu může dojít k chybě DDERR_WASSTILLDRAWING – viz dále), ale počká se na vhodný okamžik, abychom se této chybě vyhnuli. Poslední metodou třídy CWindow, kterou si ještě musíme popsat, je BltFast. Ta je proti metodě CWindow::FlipFrame z předchozího programu značně rozšířena a upravena. Důvody jsou dva. Za prvé, do front bufferu střídavě přenášíme první nebo druhý back buffer, takže tělo tohoto přenosu musíme vytvořit dvakrát pro každý back buffer zvlášť. Za druhé zde řešíme zmíněný problém ztracených surfaces. Co to vlastně ztracené surfaces jsou? K jejich ztrátě může dojít za určitých okolností. Takovým častým příkladem je, že aplikaci minimalizujeme, nebo se ve Windows dočasně přepneme do aplikace jiné (ztratíme exkluzivní přístup do paměti na grafické kartě). Pokud něco takového zkusíte provést u předchozí aplikace, zjistíte, že se texty na obrazovku již nevypisují, a to je právě z důvodu zmíněné ztráty. Ztracené surfaces se dají obnovit níže uvedeným způsobem. Nejprve ale výpis celé metody CWindow::BltFast: void CWindow::BltFast(void) { if(m_lpDDFront) { HRESULT ddrval; WriteText(); if(!m_bDisplay) { while(TRUE) { if((ddrval = m_lpDDFront->BltFast(0, 0, m_lpDDBack1, &m_ddRect, DDBLTFAST_NOCOLORKEY)) == DD_OK) break; if(ddrval == DDERR_SURFACELOST) { if((m_lpDDFront->Restore()) != DD_OK) break; }
3. DirectDraw
Prodáno 14.10.2014 na www.Kosmas.cz zákazníkovi [email protected], číslo objednávky 591522, UID: 7e15c933-e4b8-4e2a-94a6-c3fd2dffdec9
DIRECTX
47
if(ddrval != DDERR_WASSTILLDRAWING) break; } m_bDisplay = true; } else { while(TRUE) { if((ddrval = m_lpDDFront->BltFast(0, 0, m_lpDDBack2, &m_ddRect, DDBLTFAST_NOCOLORKEY)) == DD_OK) break; if(ddrval == DDERR_SURFACELOST) { if((m_lpDDFront->Restore()) != DD_OK) break; } if(ddrval != DDERR_WASSTILLDRAWING) break; } m_bDisplay = false; }
Na začátku této metody se zavoláním metody CWindow::WriteText nejprve vypíší texty do obou back bufferů. Potom se v závislosti na hodnotě proměnné m_bDisplay předává řízení pro přenos prvního nebo druhého back bufferu do front bufferu. Oba bloky jsou téměř identické. Všimněme si nekonečné smyčky (while(true)). Uvnitř této smyčky probíhá přenos příslušného back bufferu do front bufferu a dále zde je i ošetření těchto ztracených surfaces. Pro přenos dat nyní nepoužíváme metodu IDirectDrawSurface7::Blt (jako u vymazání), ale IDirectDrawSurface7::BltFast. Metodu IDirectDrawSurface7::Blt bychom samozřejmě mohli použít také, popišme si však metodu BltFast a její parametry. Jak vyplývá z názvu těchto metod, BltFast by měla být rychlejší. Zdroje firmy Microsoft udávají, že by rychlost by měla být vyšší asi o 10 %, pokud se přenos provádí softwarově (HEL). Při hardwarovém přenosu jsou rychlosti stejné. První dva parametry metody IDirectDrawSurface7::BltFast jsou x-ové a y-ové souřadnice bodu na cílovém surface, kam se začne ukládat blok ze zdrojového surface. Třetím parametrem je objekt rozhraní IDirectDrawSurface, odkud se má blok přenášet (tedy m_lpDDBack1 nebo m_lpDDBack2). Čtvrtým parametrem je reference na strukturu RECT udávající obdélníkovou oblast na zdrojovém surface, která se má přenést (my přenášíme celou obrazovku). Posledním parametrem jsou příznaky přenosu. Příznak DDBLTFAST_NOCOLORKEY říká, že se nebudou používat barevné klíče. O barevných klíčích pojednává podkapitola 3.6. V tuto chvíli si jen řekněme, že se barevné klíče používají u průhlednosti – a my zde nyní žádnou nemáme.
3. DirectDraw
} }
Dále se podívejme na návratovou hodnotu metody IDirectDrawSurface7::BltFast. Podobně jako většina metod rozhraní DirectDraw je i zde návratová hodnota typu HRESULT. Tato hodnota nám říká, zda přenos proběhl v pořádku (návratová hodnota je DD_OK a nekonečný cyklus se ukončí), nebo pokud došlo k chybě, tak návratová hodnota obsahuje kód této chyby. Jednou z chyb může být zmíněná ztráta surfaces. V takovém případě metoda IDirectDrawSurface7::Blt (resp. IDirectDrawSurface7::BltFast)
3.2 Triple buffering
Prodáno 14.10.2014 na www.Kosmas.cz zákazníkovi [email protected], číslo objednávky 591522, UID: 7e15c933-e4b8-4e2a-94a6-c3fd2dffdec9
48
DIRECTX
vrací chybu DDERR_SURFACELOST. Obnovení příslušného surface provádí metoda Restore, která nemá žádné parametry. Pokud proběhlo obnovení úspěšně, vrací se hodnota DD_OK. V takovém případě smyčku ještě nemůžeme opustit, neboť se musí provést přenos dat do obnoveného surface znovu. Jiným důvodem, proč se nepodařilo přenést data, může být chyba DDERR_WASSTILLDRAWING. Pokud k této chybě došlo, můžeme si všimnout, že v programu „nic neděláme“ – prostě se pokračuje dál. Tato chyba znamená, že je cílový surface dočasně nedostupný, nebo se čeká na synchronizaci. Pokud bychom chtěli mít program napsaný tak, aby probíhal efektivně, mohli bychom zde ještě přidat nějaký jiný kód programu, aby se vyplnil čas, po který se čeká.
3.3 DirectDraw v okně V této kapitole si upravíme náš program tak, aby neběžel na celé obrazovce, ale v klasickém okně Windows. Zůstanou přitom zachovány všechny ostatní vlastnosti aplikace, které jsme popsali v podkapitole 3.2. Zachovány tedy zůstanou i všechny soubory našeho projektu – v nich pouze provedeme určité změny, které si tu popíšeme. Do čtveřice souborů Global.h, WinMain.cpp, CApplication.h a CApplication.cpp vůbec nezasáhneme (až na jedinou výjimku – metoda pro přenos bloků mezi surface se nebude jmenovat CWindow::BltFast, ale jen CWindow::Blt podle názvu přenosové metody, kterou použijeme). Veškeré změny se tedy dotknou třídy CWindow a obsahů jejích metod. Třída CWindow v souboru CWindow.h bude nyní vypadat takto: class CWindow { private: HWND m_hWnd; LPDIRECTDRAW7 m_lpDD; LPDIRECTDRAWSURFACE7 m_lpDDFront, m_lpDDBack1, m_lpDDBack2; RECT m_ddRect, m_ddWindowRect; bool m_bDisplay; public: CWindow(void); ~CWindow(void); bool Initialize(void); void Terminate(void); void Blt(void); void WriteText(void); void ClearScreen(DWORD dwColor = 0); friend LRESULT CALLBACK WndProc(HWND m_hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam); }; Kromě změnu názvu metody BltFast na Blt je jedinou změnou to, že nám přibyl nový atribut – struktura typu RECT (m_ddWindowRect). Řekli jsme si, že ve struktuře m_ddRECT máme uloženy rozměry obrazovky. To by nám teoreticky mohlo stačit i nyní. Ve skuteč-
3. DirectDraw
Prodáno 14.10.2014 na www.Kosmas.cz zákazníkovi [email protected], číslo objednávky 591522, UID: 7e15c933-e4b8-4e2a-94a6-c3fd2dffdec9
DIRECTX
49
nosti ale s každou aplikací běžící v okně můžeme manipulovat – v tuto chvíli mám na mysli především změnu polohy okna aplikace. V takovém případě souřadnice na cílovém surface nebudou souhlasit se souřadnicemi na zdrojovém surface. Proto je nyní potřebujeme uchovávat zvlášť. Dále si popišme změny v jednotlivých metodách, v nichž došlo ke změnám. Začneme metodou CWindow::Initialize. Mezi lokálními proměnnými tentokrát přibudou dvě proměnné typu int – cx a cy. Naopak ubude ddscaps typu DDSCAPS2, což jsme dříve používali ke tvorbě back bufferů. Ty se budou nyní vytvářet trochu odlišně (vlastně bychom ani neměli hovořit o back bufferech, ale o tzv. „off-screen surfaces“ – viz dále). Nejprve ale jako obvykle naplníme strukturu typu WNDCLASS potřebnými údaji a zaregistrujeme ji. Zde k žádným změnám nedošlo. Při vytváření okna aplikace zde ovšem již změny jsou. Po registraci třídy okna bude programový kód pokračovat následovně: cx = SCREEN_WIDTH+GetSystemMetrics(SM_CXSIZEFRAME)*2; cy = SCREEN_HEIGHT+GetSystemMetrics(SM_CYSIZEFRAME)*2 +GetSystemMetrics(SM_CYMENU);
Do proměnných cx a cy ukládáme celkovou velikost okna aplikace. My sice máme určeno, jak velké má okno být, ale rozměry, které udávají SCREEN_WIDTH a SCREEN_HEIGHT, jsou rozměry uživatelské části okna, tedy bez rámu kolem a bez záhlaví v horní části každé aplikace běžící v okně. Win32 funkce GetSystemMetrics vrací počet obrazových bodů (pixelů) u prvku, který je parametrem této funkce. SM_CXSIZEFRAME představuje šířku rámu okna aplikace, SM_CYSIZEFRAME výšku rámu okna aplikace a SM_CYMENU výšku záhlaví v horní části okna aplikace. Získané rozměry dosadíme do funkce vytvářející okno aplikace – CreateWindowEx. Ostatní parametry zůstávají stejné, jako když jsme vytvářeli čistou Win32 aplikaci. Tedy styl okna není WS_POPUP jako u celoobrazovkových aplikací, ale je to WS_OVERLAPPEDWINDOW.
3. DirectDraw
if(!(m_hWnd = CreateWindowEx(0, APP_NAME, APP_NAME, WS_ OVERLAPPEDWINDOW, 0, 0, cx, cy, NULL, NULL, wc.hInstance, NULL))) { sprintf(g_tcError,TEXT("Chyba při vvytváření okna aplikace!")); return (false); }
Poté vytváříme objekt rozhraní IDirectDraw. Zde k žádné změně nedojde. To, co je nové, je jen nastavení vlastnosti okna aplikace. Přitom však také vynecháme volání metody IDirectDraw::SetDisplayMode na změnu rozlišení a barevnou hloubku: if((DirectDrawCreateEx(NULL, (void**)&m_lpDD, IID_IDirectDraw7, NULL)) != DD_OK) { sprintf(g_tcError,TEXT("Nelze vytvořit objekt DirectDraw!")); return (false); } if((m_lpDD->SetCooperativeLevel(m_hWnd, DDSCL_ALLOWREBOOT | DDSCL_NORMAL)) != DD_OK) { sprintf(g_tcError,TEXT("Nelze nastavit vlastnosti DirectDraw aplikace!")); return (false); }
3.3 DirectDraw v okně
Prodáno 14.10.2014 na www.Kosmas.cz zákazníkovi [email protected], číslo objednávky 591522, UID: 7e15c933-e4b8-4e2a-94a6-c3fd2dffdec9
50
DIRECTX
U metody IDirectDraw::SetCooperativeLevel můžeme pozorovat rozdíly u nastavení příznaků. Chybí DDSCL_FULLSCREEN, protože tentokrát máme okenní aplikaci, a místo DDSCL_EXCLUSIVE zde máme příznak DDSCL_NORMAL. Tento příznak je charakteristický pro každou typickou aplikaci Windows, běžící v okně. Dále v metodě CWindow::Initialize nastavíme velikosti uživatelské části okna aplikace strukturám m_ddRect a m_ddWindowRect (na začátku obsahují stejné údaje) a vytvoříme všechny buffery: GetClientRect(m_hWnd, &m_ddRect); GetClientRect(m_hWnd, &m_ddWindowRect); ZeroMemory(&ddsd, sizeof(ddsd)); ddsd.dwSize = sizeof(ddsd); ddsd.dwWidth = SCREEN_WIDTH; ddsd.dwHeight = SCREEN_HEIGHT; ddsd.dwFlags = DDSD_CAPS; ddsd.ddsCaps.dwCaps = DDSCAPS_PRIMARYSURFACE; if((m_lpDD->CreateSurface(&ddsd, &m_lpDDFront, NULL)) != DD_OK) { sprintf(g_tcError,TEXT("Nelze vytvořit front buffer!")); return (false); } ZeroMemory(&ddsd, sizeof(ddsd)); ddsd.dwSize = sizeof(ddsd); ddsd.dwWidth = SCREEN_WIDTH; ddsd.dwHeight = SCREEN_HEIGHT; ddsd.dwFlags = DDSD_WIDTH | DDSD_HEIGHT | DDSD_CAPS; ddsd.ddsCaps.dwCaps = DDSCAPS_OFFSCREENPLAIN; if((m_lpDD->CreateSurface(&ddsd, &m_lpDDBack1, NULL)) != DD_OK) { sprintf(g_tcError,TEXT("Nelze vytvořit první back buffer!")); return (false); } if((m_lpDD->CreateSurface(&ddsd, &m_lpDDBack2, NULL)) != DD_OK) { sprintf(g_tcError,TEXT("Nelze vytvořit druhý back buffer!")); return (false); } Při vytváření front bufferu nejprve naplňujeme položky struktury ddsd. Opět se zaměřme pouze na rozdíly proti dřívějším aplikacím. Nově naplňujeme položky dwWidth a dwHeight rozměry okna aplikace. To je nutné, aby bylo programu zřejmé, kolik paměti se má front bufferu vyhradit (v celoobrazovkovém režimu jsme nemuseli tento údaj uvádět). Dále má položka dwFlags nastaven pouze příznak DDSD_CAPS, ale chybí ji DDSD_BACKBUFFERCOUNT. Je to z toho důvodu, že back buffery sice vytváříme, ale nejsou stejné jako v celoobrazovkových aplikacích. Zde budou back buffery nezávislé na front bufferu, budou tedy existovat samostatně. Z téhož důvodu chybí u položky ddsCaps.dwCaps příznaky DDSCAPS_FLIP a DDSCAPS_COMPLEX. Po tomto nastavení vytváříme front buffer obvyklým způsobem.
3. DirectDraw
Prodáno 14.10.2014 na www.Kosmas.cz zákazníkovi [email protected], číslo objednávky 591522, UID: 7e15c933-e4b8-4e2a-94a6-c3fd2dffdec9
DIRECTX
51
Oba back buffery nyní vytváříme podobně jako front buffer. Ostatně vidíme, jaké jejich vlastnosti se nastavují ve struktuře ddsd. Rozdíly jsou pouze u položek dwFlags a ddsCaps.dwCaps. U prvně jmenovaného jsou navíc příznaky DDSD_WIDTH a DDSD_HEIGHT udávající, že odpovídající položky těchto dvou příkazů jsou platné. A u položky ddsCaps. dwCaps používáme místo DDSCAPS_PRIMARYSURFACE příznak DDSCAPS_OFFSCREENPLAIN. Důvodem je, že nepůjde o typický back buffer, ale o tzv. off-screen surface, což si můžeme představit jako paměťový blok, kam se ukládají zadané grafické prvky (obrázky, texty apod.). Více si však o tomto tématu řekneme v následující podkapitole. Tímto metoda CWindow::Initialize končí. Dále si popíšeme metodu CWindow::Terminate, která se volá při ukončení programu. Vzhledem k tomu, že nyní nejsou naše back buffery asociovány s front bufferem, neměli bychom zapomenout na zrušení těchto objektů: void CWindow::Terminate(void) { RELEASE_DX_OBJECT(m_lpDDBack1); RELEASE_DX_OBJECT(m_lpDDBack2); RELEASE_DX_OBJECT(m_lpDDFront); RELEASE_DX_OBJECT(m_lpDD); CloseWindow(m_hWnd); Metody CWindow::WriteText a CWindow::ClearScreen nebyly nikterak změněny. Proto ještě charakterizujme poslední metodu – CWindow::Blt: void CWindow::Blt(void) { GetClientRect(m_hWnd, &m_ddWindowRect); ClientToScreen(m_hWnd, (POINT*)&m_ddWindowRect.left); ClientToScreen(m_hWnd, (POINT*)&m_ddWindowRect.right);
3. DirectDraw
}
if(m_lpDDFront) { if(!m_bDisplay) { m_lpDDFront->Blt(&m_ddWindowRect, m_lpDDBack1, &m_ddRect, DDBLT_WAIT, NULL); m_bDisplay = true; } else { m_lpDDFront->Blt(&m_ddWindowRect, m_lpDDBack2, &m_ddRect, DDBLT_WAIT, NULL); m_bDisplay = false; } } } Pro přenos dat mezi surfaces používáme místo IDirectDrawSurface7::BltFast metodu IDirectDrawSurface7::Blt. Důvodem je především lepší implementace parametrů (obdélníková oblast ze zdrojového surface se přenese do obdélníkové oblasti cílového surface). Ostatně – je to i důvod, proč používáme dvě struktury typu RECT. Všimněte si
3.3 DirectDraw v okně
Prodáno 14.10.2014 na www.Kosmas.cz zákazníkovi [email protected], číslo objednávky 591522, UID: 7e15c933-e4b8-4e2a-94a6-c3fd2dffdec9
52
DIRECTX
také toho, že zde nezjišťujeme, zda došlo ke ztrátě surfaces. To u aplikace, která běží v okně, dělat nemusíme – v tomto případě se o obnovu postarají samotná Windows. Na začátku metody CWindow::Blt zjišťujeme velikost okna naší aplikace. To je nutné, protože tyto hodnoty mohou být pokaždé jiné – velikost a polohu okna pochopitelně můžeme změnit. Poté ještě voláme dvakrát Win32 funkci ClientToScreen. Tato funkce převádí souřadnice bodu na souřadnice absolutní. Důvodem volání těchto funkcí je, že máme ve strukturách uloženy souřadnice vztažené ke klientské oblasti. U zdrojového surface nám to nevadí, ale u front bufferu potřebujeme absolutní souřadnice, tedy souřadnice vztažené k levému hornímu rohu obrazovky. Prvním parametrem funkce ClientToScreen je handle okna aplikace, u které chceme převod provést, druhým parametrem je ukazatel na strukturu typu POINT, která obsahuje klientské souřadnice, jež se mají převést. Zbytek metody Blt by měl být srozumitelný, neboť přenášíme obsah prvního nebo druhého back bufferu do front bufferu pomocí metody IDirectDrawSurface7::Blt, kterou jsme popsali již dříve.
3.4 Off-screen surfaces Nyní opustíme tvorbu aplikace v okně a vrátíme se k celoobrazovkovému režimu s jedním back bufferem. Od textu přejdeme ke grafice. Ukážeme si, jak pomocí DirectDraw zobrazit obrázek – pokud máte k dispozici zdrojové kódy programů k této knize, naleznete zde fotografii malých kamínků. Zde uvedený postup ovšem můžete použít i pro jiný libovolný obrázek. Obrázky jsou v DirectDraw reprezentovány jako tzv. off-screen surfaces. Stejně jako u bufferů jde o určitý úsek paměti, kde je příslušný obrázek uložen (může to být v paměti počítače nebo grafické karty). Jednotlivé grafické prvky, které chceme v našem programu používat, mohou být reprezentovány odděleně jako externí soubory, nebo mohou být přímou součástí zkompilovaného programu. My použijeme první způsob. V praxi se grafické informace do obrázků ukládají různými způsoby. Můžeme se tak setkat s příklady, kdy může být každý obrázek uložen v samostatném souboru. V jiných případech se zase související grafika (například fáze spritové animace) vkládá do jednoho souboru. Oba tyto způsoby mají svoje výhody i nevýhody. V prvním případě máme souborů více (načítání tedy trvá déle), na druhou stranu pak se surfaces, které nám tyto obrázky reprezentují, manipulujeme zpravidla jako s celky, práce s nimi je tedy jednodušší. Druhý způsob naopak představuje komplikaci v tom, že pracujeme s částmi surfaces a musíme tedy vždy vědět, kde přesně se v surface potřebná část grafického prvku nachází. Ještě než se pustíme do programování, přibližme si vztahy off-screen surface, back bufferu a front bufferu. Tyto vazby nám přibližuje obrázek 3.2. Podle potřeby přenášíme buď celé off-screen surfaces, nebo jejich části do back bufferu (tečky v obrázku naznačují, že off-screen surfaces může být i velké množství). Poté, co je celý obraz zkompletován, se buď do front bufferu přenese (metoda Blt nebo BltFast), nebo se obsahy zamění (metoda Flip). Programové ošetření zobrazování obrázků nebude příliš komplikované. Pouze musíme vytvořit surface, do něhož se bude obrázek načítat, a potom vytvořit funkci, která načtení ze souboru provede. Pak již stačí vytvořený a naplněný surface přenášet do back bufferu. Pro jednoduchost budeme předpokládat 32bitovou barevnou hloubku. Práce s 8bitovou barevnou hloubkou je o něco málo složitější. Tento způsob zobrazení si popíšeme v následující kapitole. Stejně jako v předchozích příkladech, i zde zůstane zachován stejný
3. DirectDraw
Prodáno 14.10.2014 na www.Kosmas.cz zákazníkovi [email protected], číslo objednávky 591522, UID: 7e15c933-e4b8-4e2a-94a6-c3fd2dffdec9
DIRECTX
off-screen surface 1
off-screen surface 2
53
.....
Bli
it
t
Bl
Blit back buffer
(Flip)
front buffer
(obrazovka)
Obrázek 3.2: Vztahy mezi surfaces
počet i názvy souborů. Protože budeme vytvářet opět celoobrazovkovou aplikaci, změny budeme popisovat vzhledem k programu z podkapitoly 3.2. Popis začneme u souboru Global.h. Zde došlo k několika úpravám, proto si uveďme celý programový kód: #define _CRT_SECURE_NO_DEPRECATE 1 #pragma once
#pragma comment (lib,"dxguid.lib") #pragma comment (lib,"ddraw.lib") #define APP_NAME TEXT("DirectDraw aplikace") #define FILEIMAGE TEXT("Image.bmp") #define SCREEN_WIDTH #define SCREEN_HEIGHT #define SCREEN_DEPTH
3. DirectDraw
#include <windows.h> #include <stdio.h> #include
640 480 32
#define WAITING_FOR_FLIP #define RELEASE_OBJECT(p) #define RELEASE_DX_OBJECT(p)
1000 {if(p){delete p; p = NULL;};}; {if(p){(p)->Release(); p = NULL;};};
extern TCHAR g_tcError[256]; Odstranili jsme makra BACKTEXT1 a BACKTEXT2, protože již texty nebudeme vypisovat. Namísto toho máme nové makro FILEIMAGE, které obsahuje název souboru s obrázkem, jenž se bude načítat. Dále došlo ke změně barevné hloubky (SCREEN_DEPTH) na 32 bitů (tzv. režim true color, navíc s průhledností). Poslední změnou je přidání nového makra RELEASE_OBJECT, jež bude rušit dynamické instance objektu, který je parametrem tohoto makra. Přinese nám to zjednodušení programového kódu v dalších programech, kdy budeme mít dynamických objektů více. Podmínka v těle tohoto makra je na místě, neboť rušený objekt musí existovat. Kdybychom se pokusili rušit neexistující objekt, program by se zhroutil. Soubor WinMain.cpp se téměř nezměnil – jedinou změnou je, že místo metody WriteText u instance App třídy CApplication voláme metodu WriteImage. Vlastně jde jen
3.4 Off-screen surfaces
Prodáno 14.10.2014 na www.Kosmas.cz zákazníkovi [email protected], číslo objednávky 591522, UID: 7e15c933-e4b8-4e2a-94a6-c3fd2dffdec9
54
DIRECTX
o kosmetickou změnu v názvu, aby přesněji vystihoval to, co tato metoda provádí. Úměrně tomu změníme i název této metody ve třídě CApplication (v souboru CApplication. h to bude jediná změna). V souboru CApplication.cpp provedeme opět jen drobnou úpravu. Ta se týká právě nové metody WriteImage. Stejnojmennou metodu vytvoříme i ve třídě CWindow. Tělo této metody ve třídě CApplication bude po úpravách vypadat následovně: void CApplication::WriteImage(void) { m_Window.WriteImage(); } Další změny proběhly ve třídě CWindow. Její obvyklé atributy (handle okna m_hWnd, objekt rozhraní IDirectDraw m_lpDD, objekty rozhraní IDirectDrawSurface m_lpDDFront, m_lpDDBack, a strukturu rozměrů okna aplikace m_ddRect) doplňuje atribut nový – m_lppBackImage, což je ukazatel na ukazatel objektu rozhraní IDirectDrawSurface. Je to vlastně stejný typ objektu jako front a back buffery. Tento nový atribut reprezentuje off-screen surface, do něhož budeme ukládat obrázek načtený ze souboru. A proč jsme použili ukazatel na ukazatel? Samozřejmě by to tak být nemuselo, ale je to trochu úmysl, abychom si ukázali, jak se v takovém případě s ukazateli pracuje. Zkušenější programátoři vědí, v čem je jejich síla. Výhodné je to například při práci s většími bloky dat. Při operacích s takovým blokem, jako je například záměna obsahů dvou bloků, je rychlejší a efektivnější zaměnit ukazatele na ně než složitě přenášet všechna data. Celá třída CWindow v souboru CWindow.cpp vypadá nyní takto: class CWindow { private: HWND m_hWnd; LPDIRECTDRAW7 m_lpDD; LPDIRECTDRAWSURFACE7 m_lpDDFront, m_lpDDBack; LPDIRECTDRAWSURFACE7 *m_lppBackImage; RECT m_ddRect; public: CWindow(void); ~CWindow(void); bool Initialize(void); void Terminate(void); void BltFast(void); void WriteImage(void); void ClearScreen(DWORD dwColor = 0); bool CreateOffscreenSurface(LPDIRECTDRAWSURFACE7 * lpDDSurface, int iWidth, int iHeight); bool LoadImageFromBMP(LPDIRECTDRAWSURFACE7 lpDDSurface, TCHAR *FileName, int iBmpWidth, int iBmpHeight); friend LRESULT CALLBACK WndProc(HWND m_hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam); };
3. DirectDraw
Prodáno 14.10.2014 na www.Kosmas.cz zákazníkovi [email protected], číslo objednávky 591522, UID: 7e15c933-e4b8-4e2a-94a6-c3fd2dffdec9
DIRECTX
55
Jako další novinku v této třídě můžete zaznamenat dvě členské metody CreateOffscreenSurface a LoadImageFromBMP. Z jejich názvů můžeme odhadovat, co dělají. První vytváří off-screen surface. První parametr této metody je ukazatel na surface, který se bude vytvářet, následující dva argumenty představují výšku a šířku tohoto surface. Metoda CWindow::LoadImageFromBMP načítá obrázek ze souboru formátu *.bmp a ukládá ho do surface, který je prvním parametrem této metody. Poslední dva parametry jsou výška a šířka tohoto obrázku. Definice všech metod třídy CWindow se opět nachází v souboru CWindow.cpp. Jako první si všimněme konstruktoru a destruktoru. Tělo konstruktoru je velice podobné jako v předchozích případech – soukromým atributům z důvodu bezpečnosti přiřazujeme hodnotu NULL. Zajímavější je tělo destruktoru, resp. metody CWindow::Terminate, která se z destruktoru volá: void CWindow::Terminate(void) { RELEASE_OBJECT(m_lppBackImage); RELEASE_DX_OBJECT(m_lpDDFront); RELEASE_DX_OBJECT(m_lpDD); CloseWindow(m_hWnd); Novinkou je, že rušíme dynamickou instanci m_lppBackImage, tedy uvolňujeme surface, kam se ukládal obrázek. Toto rušení objektu se provádí makrem RELEASE_OBJECT, které jsme popsali výše. Na rozdíl od ostatních instancí (m_lpDDFront a m_lpDD) jde o ukazatel na ukazatele, proto se musí rušit příkazem delete. Metoda CWindow::Initialize má jednu obměnu a jedno rozšíření. Změnou je, že máme pouze jeden back buffer (programový kód je tedy stejný jako u příkladu v podkapitole 3.1). Rozšíření této metody představuje vytvořený a naplněný off-screen surface:
3. DirectDraw
}
m_lppBackImage = new LPDIRECTDRAWSURFACE7; if(!CreateOffscreenSurface(m_lppBackImage, SCREEN_WIDTH, SCREEN_HEIGHT)) { sprintf(g_tcError,TEXT("Nelze vytvořit off-screen surface!")); return (false); } if(!LoadImageFromBMP(*m_lppBackImage, FILEIMAGE, SCREEN_WIDTH, SCREEN_HEIGHT)) { sprintf(g_tcError,TEXT("Nelze načíst obrázek ze souboru!")); return (false); } Operátorem new vytvoříme nový objekt m_lppBackImage a nahrajeme do něj bitmapový obrázek. Vytváření tohoto surface provádí metoda CWindow::CreateOffscreenSurface: bool CWindow::CreateOffscreenSurface(LPDIRECTDRAWSURFACE7 * lpDDSurface, int iWidth, int iHeight) { DDSURFACEDESC2 m_ddsd;
3.4 Off-screen surfaces
Prodáno 14.10.2014 na www.Kosmas.cz zákazníkovi [email protected], číslo objednávky 591522, UID: 7e15c933-e4b8-4e2a-94a6-c3fd2dffdec9
56
DIRECTX
ZeroMemory( &m_ddsd, sizeof(m_ddsd)); m_ddsd.dwSize = sizeof(m_ddsd); m_ddsd.dwFlags = DDSD_CAPS | DDSD_HEIGHT | DDSD_WIDTH; m_ddsd.ddsCaps.dwCaps = DDSCAPS_OFFSCREENPLAIN; m_ddsd.dwWidth = iWidth; m_ddsd.dwHeight = iHeight; if((m_lpDD->CreateSurface(&m_ddsd, lpDDSurface, NULL)) != DD_OK) return (false); return (true); } Při podrobnějším pohledu do těla této metody by nám měl tento programový kód připadat povědomý. Je totiž naprosto identický s tím, jak jsme vytvářeli oba back buffery v aplikaci běžící v okně. Tehdy jsme si řekli, že vlastně ani o back buffery nešlo, a všechny příkazy jsme popsali. V tuto chvíli tedy tyto příkazy nemusíme dále komentovat. Nový algoritmus skrývá metoda CWindow::LoadImageFromBMP: bool CWindow::LoadImageFromBMP(LPDIRECTDRAWSURFACE7 lpDDSurface, TCHAR *FileName, int iBmpWidth = SCREEN_WIDTH, int iBmpHeight = SCREEN_HEIGHT) { BITMAP bmp; HBITMAP hBmp; HDC hDc, hDcImage; DDSURFACEDESC2 ddsd; if((lpDDSurface->Restore()) != DD_OK) return false; if(!(hBmp = (HBITMAP)LoadImage(NULL, FileName, IMAGE_BITMAP, iBmpWidth, iBmpHeight, LR_LOADFROMFILE|LR_CREATEDIBSECTION))) return (false); if(!(hDcImage = CreateCompatibleDC(NULL))) return (false); SelectObject(hDcImage, hBmp); GetObject(hBmp, sizeof(BITMAP), &bmp); ddsd.dwSize = sizeof(ddsd); ddsd.dwFlags = DDSD_HEIGHT | DDSD_WIDTH; lpDDSurface->GetSurfaceDesc(&ddsd); if((lpDDSurface->GetDC(&hDc))!=DD_OK) return (false); if(!(StretchBlt(hDc, 0, 0, ddsd.dwWidth, ddsd.dwHeight, hDcImage, 0, 0, bmp.bmWidth, bmp.bmHeight, SRCCOPY))) return (false);
3. DirectDraw
Prodáno 14.10.2014 na www.Kosmas.cz zákazníkovi [email protected], číslo objednávky 591522, UID: 7e15c933-e4b8-4e2a-94a6-c3fd2dffdec9
DIRECTX
57
if((lpDDSurface->ReleaseDC(hDc))!=DD_OK) return (false); DeleteObject(hBmp); DeleteDC(hDcImage); return true; }
Následuje kód, který se týká DirectDraw. Dosud neznámou a nepopsanou je pro nás metoda IDirectDrawSurface7::GetSurfaceDesc. Jejím smyslem je do struktury ddsd (typ DDSURFACEDESC2) uložit informace o surface, u kterého je tato metoda volána. Nám jde konkrétně o hodnoty výšky a šířky tohoto surface, aby se sem uložil obrázek jen o takové velikosti, která se sem vejde. Uložení obrázku do surface provádí Win32 API funkce StretchBlt. Pro tuto funkci potřebujeme hdc surface, do kterého se bude zapisovat. Proto se před voláním této funkce získává (metoda IDirectDrawSurface7::GetDC) a po provedení této operace se zase uvolňuje (metoda IDirectDrawSurface7::ReleaseDC). Na úplném konci této metody se ještě ruší používané datové typy hBmp a hDcImage.
3. DirectDraw
Možná vás napadne otázka, proč načítáme formát *.bmp? Soubory uložené jako *.bmp jsou příliš veliké, což je dáno tím, že nepoužívají žádnou nebo jen jednoduchou kompresi RLE. Naproti tomu jde o jednoduchý formát, který je typický pro platformu Windows. Proto ani jeho načtení není složité naprogramovat. Při pohledu do těla této metody uvidíme, že zde z DirectDraw nenajdeme téměř nic. Většina funkcí je součástí Win32 API, proto jen stručně. Na začátku obnovíme surface, do něhož se bude ukládat obrázek (lpDDSurface>Restore()). Volání této metody je nutné z důvodu bezpečnosti. Pokud je vše v pořádku, můžeme přikročit k načítání. Načtení obrázku ze souboru provádí Win32 API funkce LoadImage. Při úspěšném načtení se vrací handle tohoto obrázku (hBmp). Potom vytváříme paměťový kontext zařízení (Win32 API funkce CreateCompatibleDC), výběr bitmapy a získání informací o bitmapě (Win32 API funkce SelectObject a GetObject).
Další metodou je CWindow::ClearScreen. Zde došlo k jediné změně: a sice, že mažeme pouze jeden back buffer: void CWindow::ClearScreen(DWORD dwColor) { DDBLTFX ddbltfx; ZeroMemory(&ddbltfx, sizeof(ddbltfx)); ddbltfx.dwSize = sizeof(ddbltfx); ddbltfx.dwFillColor = dwColor; m_lpDDBack->Blt(NULL, NULL, NULL, DDBLT_COLORFILL | DDBLT_WAIT, &ddbltfx); } Vymazání obrazovky používáme v metodě CWindow::WriteImage. V ní pouze přenášíme off-screen surface do back bufferu: void CWindow::WriteImage(void) { HRESULT ddrval; ClearScreen();
3.4 Off-screen surfaces
Prodáno 14.10.2014 na www.Kosmas.cz zákazníkovi [email protected], číslo objednávky 591522, UID: 7e15c933-e4b8-4e2a-94a6-c3fd2dffdec9
58
DIRECTX
while (TRUE) { ddrval = m_lpDDBack->BltFast(0, 0, *m_lppBackImage, &m_ddRect, DDBLTFAST_NOCOLORKEY); if(ddrval == DD_OK) break; if(ddrval == DDERR_SURFACELOST) { if((m_lpDDFront->Restore()) != DD_OK) break; if(((*m_lppBackImage)->Restore()) != DD_OK) break; else LoadImageFromBMP(*m_lppBackImage, FILEIMAGE, SCREEN_WIDTH, SCREEN_HEIGHT); } if(ddrval != DDERR_WASSTILLDRAWING) break; } } Pro přenos používáme metodu IDirectDrawSurface7::BltFast. Za pozornost stojí, že při přenosu testujeme ztracené surfaces. Neobnovujeme ale back buffer, ale front buffer a off-screen surface. Z něj totiž obrázek můžeme ztratit. Pokud tento surface obnovíme, musíme do něj obrázek znovu nahrát. Proto voláme funkci pro načtení CWindow::LoadImageFromBMP. Poslední nepopsanou metodu je CWindow::BltFast. Její tělo je velice jednoduché. Nejprve se volá metoda CWindow::WriteImage a poté se přenese obsah back bufferu do front bufferu. Zde uveďme ještě malou poznámku: při přenosu již nemusíme znovu testovat ztracené surfaces, protože to děláme v metodě CWindow::WriteImage. void CWindow::BltFast(void) { if(m_lpDDFront) { WriteImage(); m_lpDDFront->BltFast(0, 0, m_lpDDBack, &m_ddRect, DDBLTFAST_NOCOLORKEY); } } Při celkovém pohledu na celou aplikaci zjistíme, že se volá každou vteřinu. To je samozřejmě zbytečné, protože zobrazujeme stále stejný obrázek. Ve většině aplikací se ovšem na obrazovce něco děje, takže by se obsah obrazovky obměňoval tím, že by se do back bufferu ukládalo pokaždé něco jiného.
3.5 Barevná paleta Náplní této podkapitoly bude práce s barevnou paletou. Pokud u obrázků (surfaces) používáme větší než 8bitovou barevnou hloubku, tedy více než 256 barev, o paletu barev se
3. DirectDraw
Prodáno 14.10.2014 na www.Kosmas.cz zákazníkovi [email protected], číslo objednávky 591522, UID: 7e15c933-e4b8-4e2a-94a6-c3fd2dffdec9
DIRECTX
59
nemusíme vůbec zajímat. Samozřejmě je většinou výhodnější používat co nejširší barevnou škálu, ale na druhou stranu to přináší větší paměťové a časové nároky při manipulaci s nimi. Pokud tedy chceme naši aplikaci nějakým způsobem urychlit a snížit paměťové nároky, může být používání 8bitové barevné palety přínosem. Ukládáme-li nějaký 256barevný obrázek do souboru, do tohoto souboru se s ním společně ukládá i paleta barev. Její velikost bývá 768 bytů (v tomto případě jsou pro každou barvu potřeba 3 byty – složky barev červené, zelené a modré v režimu RGB). Na nás je, abychom ze souboru tuto barevnou paletu načetli a nastavili. V opačném případě se nám obrázek sice zobrazí, ale patrně nebude odpovídat barevně, protože v takovém případě se používá standardní RGB paleta, která ve většině případů nebude odpovídat obrázku. Záleží na tom, jakou barevnou paletu obrázek využívá. Dokázat si to můžeme velice jednoduše. Vezměte si program z podkapitoly 3.4. V souboru Global.h změňte hodnotu barevné hloubky (makro SCREEN_DEPTH) na 8. Poté zkuste program zkompilovat a spustit.
class CWindow { private: HWND m_hWnd; LPDIRECTDRAW7 m_lpDD; LPDIRECTDRAWSURFACE7 m_lpDDFront, m_lpDDBack; LPDIRECTDRAWSURFACE7 *m_lppBackImage; LPDIRECTDRAWPALETTE m_pDDPal;
3. DirectDraw
První úpravu v našem programu jsme již vlastně udělali (nastavení 8bitové barevné hloubky). Popišme si další úpravy, aby se na obrazovce správně zobrazoval i obrázek, který 8bitovou barevnou paletu má. V souboru Global.h v makru FILEIMAGE ještě změníme název souboru obrázku, který se bude načítat. Žádné další změny zde nebudou. Změny se nedotknou ani souborů WinMain.cpp, CApplication.h a CApplication.cpp. Úpravy se tedy budou týkat především třídy CWindow a jejích metod. Třída CWindow bude nyní vypadat následovně:
RECT m_ddRect; public: CWindow(void); ~CWindow(void); bool Initialize(void); void Terminate(void); void BltFast(void); void WriteImage(void); void ClearScreen(DWORD dwColor=0); bool CreateOffscreenSurface(LPDIRECTDRAWSURFACE7 * lpDDSurface, int iWidth, int iHeight); bool LoadImageFromBMP(LPDIRECTDRAWSURFACE7 lpDDSurface, TCHAR *FileName, int iBmpWidth, int iBmpHeight); bool CreatePaletteFromFile(LPDIRECTDRAWPALETTE *Pal, const TCHAR* FileBMP);
3.5 Barevná paleta
Prodáno 14.10.2014 na www.Kosmas.cz zákazníkovi [email protected], číslo objednávky 591522, UID: 7e15c933-e4b8-4e2a-94a6-c3fd2dffdec9
60
DIRECTX
friend LRESULT CALLBACK WndProc(HWND m_hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam); };
Přibyl nám zde nový atribut – m_pDDPal, což je objekt rozhraní IDirectDrawPalette, který bude sloužit pro uložení barevné palety. Tuto operaci bude provádět nová metoda CreatePaletteFromFile se dvěma parametry. Prvním je ukazatel na objekt palety barev, kam se uloží, druhým je jméno souboru, z něhož se má paleta načíst. Kromě nové metody CreatePaletteFromFile došlo ve třídě CWindow ke změnám obsahu tří metod. První dvě změny se týkají těl konstruktoru a destruktoru. V konstruktoru inicializujeme nový objekt m_lpDDPal na NULL: m_lpDDPal = NULL; V destruktoru ho potom rušíme: RELEASE_DX_OBJECT(m_lpDDPal); Poslední změna se týká metody CWindow::Initialize, která je rozšířena o následující kód: if(!CreatePaletteFromFile(&m_pDDPal, FILEIMAGE)) { sprintf(g_tcError,TEXT("Nelze načíst paletu ze souboru!")); return (false); } if((m_lpDDFront->SetPalette(m_pDDPal)) != DD_OK) { sprintf(g_tcError,TEXT("Nelze nastavit paletu barev!")); return (false); } Tento kód je vložen za vytvořenými front buffery a back buffery, ale ještě dříve, než jsme ze zobrazovaného obrázku vytvořili off-screen surface. Pro načtení tedy voláme metodu CWindow::CreatePaletteFromFile a pokud načtení proběhlo úspěšně, tuto paletu nastavíme. Toto nastavení provádí metoda IDirectDrawSurface7::SetPalette. Paletu stačí nastavit jen u front bufferu, tedy na obrazovce. Metoda CWindow::CreatePaletteFromFile vypadá následovně: bool CWindow::CreatePaletteFromFile(LPDIRECTDRAWPALETTE *Pal, const TCHAR* FileBMP) { PALETTEENTRY aPalette[256]; HANDLE hFile = NULL; DWORD iColor; DWORD dwColors; BITMAPFILEHEADER bf; BITMAPINFOHEADER bi; DWORD dwBytesRead; if( m_lpDD == NULL || FileBMP == NULL || Pal == NULL )
3. DirectDraw
Prodáno 14.10.2014 na www.Kosmas.cz zákazníkovi [email protected], číslo objednávky 591522, UID: 7e15c933-e4b8-4e2a-94a6-c3fd2dffdec9
DIRECTX
61
return false; *Pal = NULL; if(!(hFile=CreateFile(FileBMP, GENERIC_READ, 0, NULL, OPEN_EXISTING, 0, NULL))) return (false); ReadFile(hFile, &bf, sizeof(bf), &dwBytesRead, NULL); if(dwBytesRead != sizeof(bf)) { CloseHandle(hFile); return (false); }
ReadFile(hFile, aPalette, sizeof(aPalette), &dwBytesRead, NULL); if(dwBytesRead != sizeof(aPalette)) { CloseHandle(hFile); return (false); }
3. DirectDraw
ReadFile(hFile, &bi, sizeof(bi), &dwBytesRead, NULL); if(dwBytesRead != sizeof(bi)) { CloseHandle(hFile); return (false); }
CloseHandle(hFile); if(bi.biSize != sizeof(BITMAPINFOHEADER)) dwColors = 0; else if(bi.biBitCount > 8) dwColors = 0; else if(bi.biClrUsed==0) dwColors = 1 << bi.biBitCount; else dwColors = bi.biClrUsed; for(iColor = 0; iColor < dwColors; iColor++) { BYTE r = aPalette[iColor].peRed; aPalette[iColor].peRed = aPalette[iColor].peBlue; aPalette[iColor].peBlue = r; } if((m_lpDD->CreatePalette(DDPCAPS_8BIT, aPalette, Pal, NULL))!=DD_OK) return false; return (true); }
3.5 Barevná paleta
Prodáno 14.10.2014 na www.Kosmas.cz zákazníkovi [email protected], číslo objednávky 591522, UID: 7e15c933-e4b8-4e2a-94a6-c3fd2dffdec9
62
DIRECTX
Její tělo je poněkud delší a podobně jako u metody CWindow::LoadImageFromBMP se většina kódu netýká DirectDraw, proto nebudeme příkazy popisovat příliš podrobně. Po počátečních kontrolách, zda je vše v pořádku, se volá funkce Win32 API CreateFile, která se snaží otevřít soubor obrázku. V případě úspěchu vrací jeho handle. Potom načítáme hlavičku, informační blok ze souboru a vlastní barevnou paletu (pořadí je dáno strukturou formátu *.bmp). K načítání těchto informací se používá funkce ReadFile (je rovněž součástí Win32 API). Následuje cyklus, jehož smyslem je načtenou barevnou paletu převést na správnou RGB (původně jsou barvy zaměněné – BGR). Na konci metody CWindow::CreatePaletteFromFile vytváříme paletu pomocí metody IDirectDraw::CreatePalette, která má čtveřici parametrů. Prvním jsou příznaky udávající vlastnosti palety. Hodnota DDPCAPS_8BIT říká, že jde o 8bitovou barevnou paletu. Druhým parametrem je struktura typu PALETTEENTRY, přesněji řečeno pole prvků této struktury (podle počtu barev jich je 2, 4, 16 nebo 256 – to je náš případ), ve které jsou uloženy RGB barvy, jež se mají nastavit. Třetím parametrem je objekt rozhraní IDirectDrawPalette, který bude paletu barev reprezentovat. Poslední parametr se nepoužívá, proto je zde uvedena hodnota NULL. Původně byl určen pro budoucí použití při začlenění do rozhraní COM.
3.6 Barevný klíč Jsme u poslední podkapitoly věnované DirectDraw. Mohli bychom toho o této části DirectX napsat mnohem více, ale zřejmě by to neznamenalo žádný větší přínos. Jak bylo řečeno na začátku, v dnešní době se DirectDraw již příliš nepoužívá, protože ho s rozvojem možností počítačů vystřídal Direct3D. Pokud hledáte nějaké další informace o DirectDraw, naleznete je u starších DirectX SDK nebo na různých internetových stránkách (některé zdroje jsou uvedeny na konci této knihy). Také bychom neměli zapomenout zmínit se o tom, že ve verzi DirectX8 SDK (a ve straších verzích DirectX9 SDK) můžeme nalézt soubory s názvy ddutil.h a ddutil.cpp. Obsahují dvě třídy – CDisplay a CSurface. Tyto třídy a jejich metody slouží pro práci se všemi rozhraními DirectDraw. Je tak možné si usnadnit programátorskou práci a získat množství inspirace. Ale vraťme se k tématu této podkapitoly. Její nadpis zní „Barevný klíč“. Tento pojem souvisí se všemi surfaces. Při přenosu libovolného surface do jiného můžeme nastavit některé části průhledné, nebo u cílového surface naopak nastavit části, které se překreslovat nemají. Vytvoříme si zde poněkud rozsáhlejší program, kdy na pozadí budeme mít obrázek z vesmíru a v závislosti na náhodných hodnotách se v popředí bude v rozmezí celé obrazovky pohybovat raketa (viz obrázek 3.3). Celá problematika je složitější především v tom, že zde zavedeme nový prvek – třídu CSprite. Je to kvůli případnému snadnějšímu budoucímu rozšiřování programu. Sprite je pojem, který v programování představuje obrázek. Tento obrázek může být statický nebo třeba pohybující se, ale přitom zůstává stále stejný (například šíp vystřelený z luku v počítačové hře). Častěji se ale můžeme setkat s tzv. animovaným sprite, což je obrázek, který se s časem mění (například chůze, nebo se během pohybu rakety mění množství plamenů z jejích trysek apod.). Raketa se v našem programu pohybovat sice bude, ale pro jednoduchost se bude zobrazovat stále stejně. O animovaný sprite tedy nepůjde.
3. DirectDraw
Prodáno 14.10.2014 na www.Kosmas.cz zákazníkovi [email protected], číslo objednávky 591522, UID: 7e15c933-e4b8-4e2a-94a6-c3fd2dffdec9
DIRECTX
63
Pro náš program budeme potřebovat dva obrázky – pozadí (vesmír), jehož velikost bude odpovídat rozlišení obrazovky, a potom obrázek rakety. Oba si můžete zkusit vytvořit v nějakém grafickém editoru, nebo můžete použít obrázky dodané s touto knihou. Jmenují se BackImage.bmp a SpaceCraft.bmp. Opět nebudeme popisovat celý programový kód, ale zaměříme se pouze na změny. Koncepce programu je taková, že instanci třídy CSprite (raketa) budeme agregovat do třídy CWindow. Tato vazba je vytvořena kvůli jednoduchosti (ze třídy CSprite potřebujeme mít přístup k objektu m_lpDD). V aplikaci, která by obsahovala desítky nebo stovky spritů, by to bylo pochopitelně složitější a správa instancí třídy CSprite by spíše příslušela třídě CApplication nebo nějaké nové třídě, jejíž smyslem by byla právě práce s více instancemi třídy CSprite. Protože nebudeme omezovat náš program na 8bitovou barevnou hloubku, vyjdeme z programu popsaného v podkapitole 3.4. Jako obvykle začneme souborem Global.h. Do něj jsme navíc vložili nový hlavičkový soubor – time.h. Důvod je takový, že z něj budeme používat funkce generátoru náhodných čísel. Tato náhodná čísla budou určovat směr pohybu rakety. Dále jsme odstranili makro FILEIMAGE a místo něj vložili dvojici nových maker BACKIMAGE a SPRITE definující grafické soubory, které budeme načítat: TEXT("BackImagebmp") TEXT("SpaceCraft.bmp")
Dále došlo ke změně smyslu symbolické konstanty WAITING_FOR_FLIP. Její hodnota dříve udávala, jak často se má překreslovat obrazovka, tato hodnota byla v milisekundách. Nyní bude udávat počet aktualizací obrazovky během jedné milisekundy. Čím vyšší hodnotu zde zadáme, tím se bude raketa pohybovat rychleji. V souvislosti s tímto přibylo ještě nové makro SPRITE_TIME_DIRECTION, které udává, kolik milisekund drží raketa jeden směr. Se zmenšující se hodnotou bude raketa měnit směr častěji. Počáteční nastavení je následující: #define WAITING_FOR_FLIP #define SPRITE_TIME_DIRECTION
3. DirectDraw
#define BACKIMAGE #define SPRITE
5 200
Obrázek 3.3: Ukázka obrazovky aplikace – raketa ve vesmíru
3.6 Barevný klíč
Prodáno 14.10.2014 na www.Kosmas.cz zákazníkovi [email protected], číslo objednávky 591522, UID: 7e15c933-e4b8-4e2a-94a6-c3fd2dffdec9
64
DIRECTX
V souboru WinMain.cpp došlo ke dvěma změnám. Na začátku inicializujeme generování náhodných čísel: srand((unsigned)time(NULL)); Potom jsme ještě změnili název metody, která provádí zápis do back bufferu. Dříve jsme zde zapisovali pouze jeden obrázek, proto metoda nesla název WriteImage. Nyní však zapisujeme do surface obrázek i sprite, proto došlo ke změně názvu na WriteIntoBackBuffer. Způsob volání ovšem zůstal stejný. Ke dvěma úpravám došlo také v souboru CApplication.h. Přibyla jedna členská proměnná typu int s názvem m_iCounter, která bude sloužit jako čítač počtu aktualizací obrazovky v čase. Druhou změnou je zmíněná změna názvu metody WriteImage na WriteIntoBackbuffer. V definici metod třídy CApplication (soubor CApplication.cpp) došlo k úpravě v konstruktoru, kde inicializuje novou členskou proměnnou m_iCounter na hodnotu 0. Další změna je v metodě WriteIntoBackbuffer, kde voláme stejnojmennou metodu u instance m_Window (u třídy CWindow jsme tuto metodu také přejmenovali). K větší obměně došlo v metodě MainLoop: void CApplication::MainLoop(void) { if(m_iCounter < WAITING_FOR_FLIP) { m_iCounter++; m_Window.BltFast(); } else { m_lCurTick = GetTickCount(); if(m_lCurTick-m_lPrevTick >= WAITING_FOR_FLIP) { m_lPrevTick = m_lCurTick; m_iCounter = 0; } } } Na začátku inkrementujeme čítač a voláme metodu BltFast instance m_Window. Tato metoda provede veškeré změny v programu (tedy výpočet nové polohy rakety a její nastavení) a vykreslí pozadí a následně i raketu do back bufferu, aby se jeho obsah přenesl na obrazovku. Všimněte si, čím jsou tyto operace podmíněny. Atribut m_iCounter musí být menší než hodnota makra WAITING_FOR_FLIP. V případě nesplnění této podmínky probíhá druhá část metody MainLoop, což už je vlastně nám známý kód pro zjištění dosažení času, kdy by měly být provedeny další aktualizace. V tom okamžiku se čítač m_iCounter resetuje na nulovou hodnotu. Větších změn dostála i třída CWindow a i její metody. V souboru CWindow.h vkládáme hlavičkový soubor CSprite.h. Důvod je ten, že mezi atributy této třídy přibude instance třídy CSprite, která bude reprezentovat raketu: CSprite *m_SpaceCraft; Mezi metodami třídy CWindow došlo k výše uvedené změně názvu metody WriteImage na WriteIntoBackbuffer. Přibyla i nová metoda Restore, jejímž cílem bude obnovovat ztracené surfaces.
3. DirectDraw
Prodáno 14.10.2014 na www.Kosmas.cz zákazníkovi [email protected], číslo objednávky 591522, UID: 7e15c933-e4b8-4e2a-94a6-c3fd2dffdec9
DIRECTX
65
V tělech členských metod třídy CWindow došlo k četným změnám. V konstruktoru z důvodu bezpečnosti nastavujeme na hodnotu NULL i nový atribut m_SpaceCraft. Tento nový objekt budeme vytvářet dynamicky, proto se bude dynamicky i rušit. Z tohoto důvodu provedeme v destruktoru této třídy (resp. v metodě CWindow::Terminate) uvolnění volání makra RELEASE_OBJECT: RELEASE_OBJECT(m_SpaceCraft); Tělo metody CWindow::Initialize zůstane téměř nezměněné, pouze při vytváření off-screen surface obrázku na pozadí změníme název makra obsahujícího soubor, ze kterého se má obrázek načíst (BACKIMAGE). Na konec inicializační metody ještě přidáme kód pro vytvoření objektu m_SpaceCraft. Prvním parametrem volané metody CSprite::Initialize je název souboru, odkud se má sprite načíst, potom následuje počáteční poloha sprite (uprostřed obrazovky) a x-ové a y-ové rozměry sprite. Předposlední argument je reference na objekt rozhraní IDirectDraw (nutné pro vytvoření surface), poslední parametr je barevný klíč (viz dále):
Malou obměnou prošla metoda CWindow::BltFast, která ve svém těle místo CWindow:: WriteImage volá metodu CWindow::WriteIntoBackbuffer. Nedošlo pouze k změně názvu, ale i obsahu. Celé tělo metody WriteIntoBackbuffer vypadá následovně: void CWindow::WriteIntoBackbuffer(void) {
3. DirectDraw
m_SpaceCraft = new CSprite; if(!m_SpaceCraft->Initialize(SPRITE, SCREEN_WIDTH/2, SCREEN_HEIGHT/2, 64, 64, &m_lpDD, RGB(0, 0, 0))) { sprintf(g_tcError,TEXT("Nelze vytvořit objekt rakety!")); return (false); }
HRESULT ddrval; ClearScreen(); while (TRUE) { ddrval = m_lpDDBack->BltFast(0, 0, *m_lppBackImage, &m_ddRect, DDBLTFAST_NOCOLORKEY); if(ddrval == DD_OK) break; if(ddrval == DDERR_SURFACELOST) { if(!Restore()) break; } if(ddrval != DDERR_WASSTILLDRAWING) break; } m_SpaceCraft->Update();
3.6 Barevný klíč
Prodáno 14.10.2014 na www.Kosmas.cz zákazníkovi [email protected], číslo objednávky 591522, UID: 7e15c933-e4b8-4e2a-94a6-c3fd2dffdec9
66
DIRECTX
while (TRUE) { ddrval = m_lpDDBack->BltFast(m_SpaceCraft->GetXPos(), m_SpaceCraft->GetYPos(), m_SpaceCraft->GetSurface(), m_SpaceCraft->GetSpriteRect(), DDBLTFAST_SRCCOLORKEY); if(ddrval == DD_OK) break; if(ddrval == DDERR_SURFACELOST) { if(!Restore()) break; } if(ddrval != DDERR_WASSTILLDRAWING) break; } } Po vymazání obrazovky známým způsobem vykreslíme do back bufferu obrázek pozadí. Toto vykreslení je v nekonečném cyklu se zpracováním případných ztracených surfaces. Rozdíl je pouze ten, že jejich obnovení se neprovádí přímo, ale nově se volá metoda CWindow::Restore. Důvod je ten, že stejné obnovení se provádí v programu vícekrát – bude se provádět i při vykreslení rakety do back bufferu. Ušetříme tak programový kód. Vykreslení rakety provádí druhá část metody CWindow::WriteIntoBackbuffer. Zde je pro nás novinkou poslední parametr. Namísto DDBLTFAST_NOCOLORKEY zde máme DDBLTFAST_SRCCOLORKEY. Rozdíl je obrovský a dostáváme se k jádru názvu této podkapitoly. Parametr DDBLTFAST_SRCCOLORKEY říká, že se má brát ohled na zdrojový barevný klíč. Tento klíč se používá k nastavení barvy, která se nemá přenášet. V každém obrázku si tedy můžeme nastavit barvu, jež bude znamenat průhlednost. I když přenášíme samé obdélníkové plochy, můžeme tímto způsobem přenášet obrázky libovolného tvaru. V tuto chvíli nám toto vysvětlení postačí. O tom, jak se nastavuje barevný klíč surface, si řekneme více u popisu třídy CSprite. Ještě než vykreslíme raketu do back bufferu, všimněte si, že se provede aktualizace polohy rakety na obrazovce (příkaz m_SpaceCraft->Update()). Tělo metody CWindow::Restore obsahuje pokus o obnovení front bufferu, obrázku na pozadí a rakety. Pokud je to potřeba, oba obrázky se opětovně načtou ze souborů: bool CWindow::Restore(void) { if((m_lpDDFront->Restore()) != DD_OK) return (false); if(((*m_lppBackImage)->Restore()) != DD_OK) return (false); LoadImageFromBMP(*m_lppBackImage, BACKIMAGE, SCREEN_WIDTH, SCREEN_HEIGHT); m_SpaceCraft->LoadSpriteFromBMP(SPRITE, 72, 72); return (true); }
3. DirectDraw
Prodáno 14.10.2014 na www.Kosmas.cz zákazníkovi [email protected], číslo objednávky 591522, UID: 7e15c933-e4b8-4e2a-94a6-c3fd2dffdec9
DIRECTX
67
Ostatní metody třídy CWindow zůstaly beze změn. Nyní zaměříme pozornost na novou třídu CSprite a na její metody. Třída je jednoduchá, neboť jak již bylo řečeno, raketa se sice pohybuje, ale pokaždé má stále stejný vzhled. V čase se tedy zobrazuje stále stejný obrázek. class CSprite { private: LPDIRECTDRAWSURFACE7 *m_lppDDSurface; int m_iXPos; int m_iYPos; int m_iWidth; int m_iHeight; RECT m_SpriteRect; int m_iDirection; int m_iTimeDirection;
public: CSprite(); ~CSprite(); bool Initialize(TCHAR *FileName, int iXPos, int iYPos, int iWidth, int iHeight, LPDIRECTDRAW7 *lpDD, COLORREF ColrefRgb);
3. DirectDraw
long m_lPrevTick, m_lCurTick;
void Terminate(); LPDIRECTDRAWSURFACE7& GetSurface(void) const {return *m_lppDDSurface;} RECT* GetSpriteRect(void) {return &m_SpriteRect;} int GetXPos(void) const {return m_iXPos;} int GetYPos(void) const {return m_iYPos;} bool Create(TCHAR *FileName, int iXPos, int iYPos, int iWidth, int iHeight, LPDIRECTDRAW7 *lpDD, COLORREF ColrefRgb); bool LoadSpriteFromBMP(TCHAR *FileName, int iBmpWidth, int iBmpHeight); bool SetColorKey(COLORREF dwColorKey); DWORD ConvertGDIColor(COLORREF dwGDIColor); void Update(void); };
3.6 Barevný klíč
Prodáno 14.10.2014 na www.Kosmas.cz zákazníkovi [email protected], číslo objednávky 591522, UID: 7e15c933-e4b8-4e2a-94a6-c3fd2dffdec9
68
DIRECTX
První atribut ve třídě je m_lppDDSurface (objekt rozhraní IDIRECTDRAWSURFACE7), který bude reprezentovat obrázek. Potom zde máme soukromé atributy m_iXPos a m_iYPos, kde budou uloženy souřadnice levého horního rohu sprite na obrazovce. Další dvě proměnné, m_iWidth a m_iHeight, budou uchovávat rozměry sprite (v pixelech). Přesné rozměry budou uloženy i ve struktuře m_SpriteRect (typ Rect). Tuto strukturu využíváme kvůli metodě BltFast, která ji vyžaduje jako jeden ze svých parametrů. Do proměnné m_iDirection se ukládá hodnota, jež udává, ve kterém směru se raketa aktuálně pohybuje. Může nabývat hodnot 0-4 (stop, doprava, doleva, dolů, nahoru). Poslední dvě proměnné m_lPrevTick a m_lCurTick se používají k časování pro aktualizace polohy. Dále se podívejme na metody této třídy. Kromě konstruktoru, destruktoru, počáteční inicializace (Initialize), ukončení (Terminate) a metod pro vrácení některých atributů sprite zde máme ještě LoadSpriteFromBMP (je stejná jako v předchozích programech, takže ji nebudeme popisovat znovu), SetColorKey, ConvertGDIColor a Update. Metoda CSprite::Initialize vypadá následovně: bool CSprite::Initialize(TCHAR *FileName, int iXPos, int iYPos, int iWidth, int iHeight, LPDIRECTDRAW7 *lpDD, COLORREF ColrefRgb) { DDSURFACEDESC2 ddsd2; m_lppDDSurface=new LPDIRECTDRAWSURFACE7; ZeroMemory(&ddsd2, sizeof(ddsd2)); ddsd2.dwSize = sizeof(ddsd2); ddsd2.dwFlags = DDSD_CAPS | DDSD_HEIGHT | DDSD_WIDTH; ddsd2.ddsCaps.dwCaps = DDSCAPS_OFFSCREENPLAIN; ddsd2.dwWidth = iWidth; ddsd2.dwHeight = iHeight; if(((*lpDD)->CreateSurface(&ddsd2, m_lppDDSurface, NULL))!=DD_OK) return (false); if(!LoadSpriteFromBMP(FileName, iWidth, iHeight)) return (false); if(!SetColorKey(ColrefRgb)) return (false); m_iXPos = iXPos; m_iYPos = iYPos; m_SpriteRect.left = 0; m_SpriteRect.top = 0; m_SpriteRect.right = iWidth; m_SpriteRect.bottom = iHeight; return true; } Vytváříme zde počáteční inicializace všech členských atributů a zároveň off-screen surface, který bude sprite reprezentovat. Metodou CSprite::LoadSpriteFromBMP načítáme
3. DirectDraw
Prodáno 14.10.2014 na www.Kosmas.cz zákazníkovi [email protected], číslo objednávky 591522, UID: 7e15c933-e4b8-4e2a-94a6-c3fd2dffdec9
DIRECTX
69
sprite ze souboru. Tuto metodu jsme popisovali již u předchozích programů, proto nejsou potřeba žádné další komentáře. V metodě CSprite::Terminate pouze rušíme vytvořený objekt m_lppDDSurface: void CSprite::Terminate() { RELEASE_OBJECT(m_lppDDSurface); } Nová je pro nás ovšem metoda CSprite::SetColorKey: bool CSprite::SetColorKey(COLORREF dwColorKey) { DDCOLORKEY ddck; if(m_lppDDSurface == NULL) return (false); ddck.dwColorSpaceLowValue = ConvertGDIColor(dwColorKey); ddck.dwColorSpaceHighValue = ddck.dwColorSpaceLowValue; if(((*m_lppDDSurface)->SetColorKey(DDCKEY_SRCBLT, &ddck)) != DD_OK) return (false);
Do této metody nám jako jediný parametr vstupuje barva, jež bude barevným klíčem. Pokud se podíváme na volání této funkce, zmíněnou barvu máme nastavenou jako černou (RGB(0, 0, 0)). Tato barva se ukládá do datového typu DDCOLORKEY, který má ve skutečnosti dvě položky: dwColorSpaceLowValue a dwColorSpaceHighValue. Jak vyplývá z jejich názvu, barevný klíč se nemusí nastavit pouze pro jednu barvu, ale pro určité rozmezí barev. Jedna barva se nastaví, pokud oběma těmto položkám přiřadíme stejnou hodnotu (náš případ). Nicméně barvu nemůžeme do těchto položek ukládat v režimu RGB, proto se provádí jejich převod v metodě ConvertGDIColor. Samotné nastavení barevného klíče příslušnému surface provádí metoda IDirectDrawSurface7::SetColorKey. Datový typ typu DDCOLORKEY se zadává jako druhý parametr této metody, prvním parametrem jsou vlastnosti klíčů. Nejčastěji se používají příznaky DDCKEY_DESTBLT (barevný klíč se bude při přenosu používat na cílovém surface) a DDCKEY_SRCBLT (barevný klíč se bude při přenosu používat na zdrojovém surface).
3. DirectDraw
return (true); }
Jak již bylo zmíněno, metoda CSprite::ConvertGDIColor provádí transformaci barev: DWORD CSprite::ConvertGDIColor(COLORREF dwGDIColor) { COLORREF rgbT; HDC hdc; DWORD dw = CLR_INVALID; DDSURFACEDESC2 ddsd; if(m_lppDDSurface == NULL) return (false);
3.6 Barevný klíč
Prodáno 14.10.2014 na www.Kosmas.cz zákazníkovi [email protected], číslo objednávky 591522, UID: 7e15c933-e4b8-4e2a-94a6-c3fd2dffdec9
70
DIRECTX
if((dwGDIColor != CLR_INVALID && (*m_lppDDSurface)->GetDC(&hdc)) == DD_OK) { rgbT = GetPixel(hdc, 0, 0); SetPixel(hdc, 0, 0, dwGDIColor); (*m_lppDDSurface)->ReleaseDC(hdc); } ddsd.dwSize = sizeof(ddsd); if((*m_lppDDSurface)->Lock( NULL, &ddsd, DDLOCK_WAIT, NULL) == DD_OK) { dw = *(DWORD *) ddsd.lpSurface; if(ddsd.ddpfPixelFormat.dwRGBBitCount < 32) dw &= (1 << ddsd.ddpfPixelFormat.dwRGBBitCount) - 1; (*m_lpDDSurface)->Unlock(NULL); }
if((dwGDIColor!=CLR_INVALID && (*m_lppDDSurface)->GetDC(&hdc)) == DD_OK) { SetPixel( hdc, 0, 0, rgbT); (*m_lppDDSurface)->ReleaseDC(hdc); } return dw; } Při podrobnějším pohledu na tuto metodu zjistíme, že začátek a konec této metody jsou podobné. Na začátku si uchováváme v paměti barvu pixelu levého horního rohu obrazovky (funkce GetPixel) a místo něj tam funkcí SetPixel nastavíme barvu, která do této metody vstupuje jako jediný parametr. V obou případech jde o funkce, které jsou součástí Win32API a potřebujeme k nim HDC, proto se v souvislosti s nimi na začátku volá metoda GetDC a na konci ReleaseDC. Na konci metody ConvertGDIColor vracíme původní barvu pixelu zpět. Mezitím námi nastavenou barvu zjišťujeme pomocí struktury typu DDSURFACEDESC2 (již ve formátu, který potřebujeme). Aktuálně nás zajímá její položka lpSurface, ve které je uložena adresa paměti, kde je uložený surface, s nímž je tato struktura asociována. Tuto asociaci provádí metoda Lock (tato metoda provádí ve skutečnosti „zamčení“, které je třeba, aby se během načítání nemohl surface přenášet na jiné místo paměti – na konci je odemčeno metodou Unlock). Metoda Lock má jako první parametr strukturu typu RECT udávající oblast zamčení. NULL znamená, že bude uzamčený celý surface. Druhým parametrem je struktura typu DDSURFACEDESC2, do které budou uloženy veškeré informace o příslušném surface. A třetím parametrem jsou příznaky. DDLOCK_WAIT znamená, že pokud se surface nedá aktuálně zamknout (mohou se s ním v tu chvíli zrovna provádět nějaké operace), tak se počká, dokud to nepůjde. Poslední parametr se nepoužívá a je vždy nastaven na hodnotu NULL. Poslední metodou je Update: void CSprite::Update() { if(m_iDirection)
3. DirectDraw
Prodáno 14.10.2014 na www.Kosmas.cz zákazníkovi [email protected], číslo objednávky 591522, UID: 7e15c933-e4b8-4e2a-94a6-c3fd2dffdec9
DIRECTX
71
{ if((m_iDirection == 1) && (m_iXPos < (SCREEN_WIDTH-m_iWidth-1))) m_iXPos++; else if((m_iDirection == 2) && (m_iXPos > 0)) m_iXPos--; else if((m_iDirection == 3) && (m_iYPos < (SCREEN_HEIGHT-m_iHeight-1))) m_iYPos++; else if((m_iDirection == 4) && (m_iYPos > 0)) m_iYPos--; } m_lCurTick=GetTickCount();
} Tato metoda zpracovává pohyb rakety. Nejprve zjišťujeme, jestli se raketa vůbec pohybuje (členská metoda m_iDirection). Pokud ano, v závislosti na směru se pomocí série podmínek aktualizuje její poloha. V případě, že nastal správný čas pro změnu směru pohybu rakety, se tato změna provede na konci této metody tím, že se vygeneruje náhodné číslo v rozmezí 1–4, které bude udávat její další směr.
3. DirectDraw
if((m_lCurTick-m_lPrevTick) > SPRITE_TIME_DIRECTION) { m_iDirection = (rand()%4)+1; m_lPrevTick = m_lCurTick; }
3.6 Barevný klíč
Prodáno 14.10.2014 na www.Kosmas.cz zákazníkovi [email protected], číslo objednávky 591522, UID: 7e15c933-e4b8-4e2a-94a6-c3fd2dffdec9
72
DIRECTX
3. DirectDraw
Prodáno 14.10.2014 na www.Kosmas.cz zákazníkovi [email protected], číslo objednávky 591522, UID: 7e15c933-e4b8-4e2a-94a6-c3fd2dffdec9
DIRECTX
73
4. Direct3D je souhrn rozhraní pro práci s 3D grafikou. Právě tato komponenta DirectX se v poslední době rozvíjí nejdynamičtěji a disponuje mnoha operacemi, které zajišťují širokou škálu možností vzhledu výsledného obrazu. Uveďme si alespoň stručný výčet těchto možností (viz literaturu [14]):
4. Direct3D
Direct3D
Plná podpora 3D prostředí, do kterého lze umísťovat libovolné objekty, od primitivních prvků jako body, čáry a polygony, až po složité komplexní objekty. Zobrazování 3D scén je možné v celoobrazovkovém režimu nebo v okně aplikace. Samozřejmostí je také možnost volby rozlišení obrazu a barevné hloubky. Rozsáhlé možnosti práce s osvětlením scény – Direct3D poskytuje mnoho typů světelných zdrojů s příslušnými vlastnostmi, které je možné nastavit (například bodová všesměrová světla, plošná směrová světla či okolní rozptýlené světlo). Samozřejmostí je i podpora výpočtů pro vykreslení stínů, které vrhají objekty osvětlené příslušnými světelnými zdroji. Každému objektu lze nastavit materiálové vlastnosti. Zde je možné jednotlivým plochám přiřazovat barvu, barevnou škálu nebo využít textur (barevných vzorů).
4. Direct3D
Prodáno 14.10.2014 na www.Kosmas.cz zákazníkovi [email protected], číslo objednávky 591522, UID: 7e15c933-e4b8-4e2a-94a6-c3fd2dffdec9
74
DIRECTX
Textury lze na objekt nanášet definovaným způsobem, mohou ovlivňovat barvu, ale například i nerovnosti či zrcadlové odrazy povrchu objektu.
Direct3D má svůj vlastní 3D grafický formát s příponou *.x, ve kterém mohou být uloženy libovolné 3D grafické objekty a jejich vlastnosti (normálové vektory, uv souřadnice pro textury, animace atd.). Direct3D přitom disponuje rozhraními s množstvím metod pro snadnou práci s tímto grafickým formátem. Velkou výhodou je, že formát *.x je podporován i mnoha 3D grafickými programy, takže je možné do něj ukládat grafické modely objektů, které chceme využít v našich aplikacích. V Direct3D je možné měnit i vlastnosti kamery. S kamerou se dají provádět standardní operace (posun, otáčení, naklápění), přiblížení či oddálení pohledu na scénu (zoom) a volit typ projekce. Samozřejmostí je, že ve scéně můžeme mít i více kamer, mezi jejichž pohledy se můžeme přepínat. Vylepšení vzhledu scény na základě podpory různých algoritmů. Příkladem takových algoritmů je atmosférický efekt mlhy nebo antialiasing (vyhlazení „zubatých“ hran).
Možnosti Direct3D jsou opravdu značné, proto bude tomuto rozhraní věnováno z celé knihy nejvíce prostoru. Tato kapitola je napsána tak, že ji můžete studovat, i když jste přeskočili část o DirectDraw. I tak však doporučuji se vrátit alespoň k úvodním informacím, které jsou uvedeny hned na začátku minulé kapitoly. Možná nebudete vytvářet DirectDraw aplikace, ale usnadní vám to pochopení i Direct3D, protože jak bylo uvedeno na začátku knihy, obě tyto komponenty DirectX mají podobnou koncepci. Hierarchické umístění rozhraní Direct3D jako celku v systému Windows popisuje obrázek 4.1 [14]. Pokud ho porovnáme s obrázkem 3.1 o DirectDraw, zjistíme, že jsou téměř totož-
Win32 Application
Direct3D
GDI
HAL
DDI (Device Driver Interface)
Hardware (Video Card) Obrázek 4.1: Integrace Direct3D do systému
4. Direct3D
Prodáno 14.10.2014 na www.Kosmas.cz zákazníkovi [email protected], číslo objednávky 591522, UID: 7e15c933-e4b8-4e2a-94a6-c3fd2dffdec9
DIRECTX
75
né. Direct3D rozhraní nahrazují standardní grafické rozhraní Windows (GDI – Graphics Device Interface). Jak GDI, tak i Direct3D jsou hardwarově nezávislé, tedy fungují stejně pro různé grafické karty. Nezávislost je zajištěna prostřednictvím DDI, což jsou vlastně nainstalované ovladače grafické karty v systému [8]. Ještě zbývá vysvětlit zkratku HAL. Představuje zkratku Hardware Abstraction Layer, což znamená hardwarové urychlení grafických operací. Jde o funkce, které umí přímo zpracovat grafická karta. Pokud je provádí, dochází ke znatelnému urychlení těchto funkcí. Ovšem ne každá karta podporuje všechny operace. Obecně můžeme říci, že čím je grafická karta novější (a tedy i dražší), tím toho dokáže více. Pokud však chceme mít jistotu hardwarové podpory konkrétní funkce, stačí zavolat jednu z metod IDirect3D9::GetDeviceCaps nebo IDirect3DDevice9::GetDeviceCaps. Pro nejrozšířenější grafické karty je možné tyto informace nalézt i v souborech CardCaps.pdf a CardCaps.xls, které jsou součástí jednoho z ukázkových programů v DirectX SDK. Stručně se ještě podívejme na vnitřní architekturu Direct3D, na tzv. pipeline. Pokud nebudete všemu rozumět, nemusíte se tím zatím příliš trápit. Lépe pochopitelné to pro vás možná bude až ve chvíli, kdy budete vytvářet praktické programy.
4. Direct3D
Blokové schéma Direct3D pipeline je uvedeno na obrázku 4.2 [14]. Prvním ze vstupů je blok Vertex data. Ten obsahuje seznam vertexů, tedy vrcholů objektů. Tyto seznamy bývají uložené v tzv. vertex bufferech. Vedle toho máme další blok Primitive Data, který zahrnuje seznamy geometrických primitiv, jako jsou body, úsečky a polygony. Protože jsou tyto primitivy dány i vertexy (například úsečka je dána počátečním a koncovým bodem (vertexem), jsou zde uvedeny i ukazatele do vertex bufferů. Oba bloky slouží jako vstup do další části nazvané Tessellation. Zde se provádí převod složitějších objektů (například křivek) na polohy vertexů a tyto polohy se následně ukládají do vertex bufferu. Poté následuje blok s názvem Vertex Processing. V této části se na vertexy uložené ve vertex bufferu aplikují potřebné transformace. V následujícím bloku (Geometry Processing) se naposledy pracuje s vertexy. Zde se totiž převádějí do pixelové podoby. Ještě předtím však dochází k ořezání neviditelných částí (části mimo zorné pole kamery nebo odvrácené části objektů, které nejsou kamerou viditelné) a poté probíhá rasterizace, tedy převod do rastrové (pixelové) podoby.
Vertex Data
Primitive Data
Tessellation
Vertex Processing
Geometry Processing
Textured Surface
Texture Sampler
Pixel Processing
Pixel Rendering Obrázek 4.2: Direct3D pipeline
4. Direct3D
Prodáno 14.10.2014 na www.Kosmas.cz zákazníkovi [email protected], číslo objednávky 591522, UID: 7e15c933-e4b8-4e2a-94a6-c3fd2dffdec9
76
DIRECTX
Následně probíhá potřebné zpracování jednotlivých pixelů (Pixel Processing), kde v závislosti na užitých algoritmech získáváme výsledek v podobě barvy každého z pixelů. Prováděné operace mohou být naprogramovány – tzv. pixel shader. Také zde mohou vstupovat ještě pixely z obrázků textur, jež také podstatným způsobem ovlivňují barvu každého z pixelů. Tato část začíná v bloku Textured Surface, kde jsou texturové souřadnice přeneseny do Direct3D prostřednictvím rozhraní IDirect3DTexture9, a dále může podle potřeby probíhat převzorkování textury (blok Texture Sampler). Na konci pipeline je blok Pixel Rendering, kde probíhá finální vykreslování scény a mohou zde být přidány některé další modifikace jako je míchání barev (takto se vytváří například průhlednost) nebo dodání efektu mlhy. U DirectDraw jsme uvedli, že můžeme vykreslovat přímo na obrazovku. Také jsme si řekli, že se to však kvůli blikání obrazu nepoužívá. Existuje tedy jeden front buffer, jehož obsah odpovídá obsahu obrazovky, a jeden či více back bufferů, do kterých ukládáme údaje, které se mají zobrazit v příštích okamžicích. Po jejich naplnění se ve správný čas obsah některého z back bufferů přenese do front bufferu, čímž uživatel na monitoru vidí nový obraz. V podstatě to samé platí i u Direct3D. Zde můžeme mít až tři back buffery (jejich množství si pochopitelně můžeme nastavit). Ve většině případů si ovšem vystačíme jen s jedním. Nyní si uveďme přehled rozhraní Direct3D, které jsou k dispozici. Nebudeme si zde uvádět všechny (v DirectX 9.0c je jich celkem 22), ale jen ty nejdůležitější, které budeme používat i v našich programech v dalších kapitolách. Jejich seznam je uveden v tabulce 4.1. V levé části je uveden název rozhraní a v pravém sloupci popis, specifikující význam objektů těchto rozhraní. Tabulka 4.1: Často užívaná rozhraní Direct3D
Rozhraní IDirect3D9
Popis Základní stavební kámen každé Direct3D aplikace, na základě objektu tohoto rozhraní se tvoří objekty jiných rozhraní.
IDirect3DDevice9
Reprezentuje zobrazovací zařízení.
IDirect3DSurface9
Paměť s uloženými 2D grafickými daty.
IDirect3DVertexBuffer9
Oblast paměti, kde jsou uloženy souřadnice vertexů.
IDirect3DIndexBuffer9
Oblast paměti, kde jsou uloženy indexy dat, uložených ve vertex bufferu.
IDirect3DTexture9
Textura, tedy 2D obrázek nanášený na 3D objekty.
4.1 Základní pojmy a transformace Na tomto místě probereme některé základní pojmy, které budeme potřebovat, až budeme vytvářet 3D aplikace. Jimi se začneme zabývat až od podkapitoly 4.5. Následující tři podkapitoly pouze popisují nejnutnější základy, které se týkají rozhraní IDirect3D9, IDirect3D9Device9 a IDirect3D9Surface9. K nim ještě nebudeme potřebovat příliš podrobné znalosti o souřadnicovém systému či transformacích, o kterých bude zmínka již zde. Při tomto popisu se opět zaměříme jen na ty informace, které budou pro nás nejdůležitější. Čtenářům, kteří by se chtěli dozvědět více teoretických informací o 3D grafice,
4. Direct3D
Prodáno 14.10.2014 na www.Kosmas.cz zákazníkovi [email protected], číslo objednávky 591522, UID: 7e15c933-e4b8-4e2a-94a6-c3fd2dffdec9
DIRECTX
77
doporučuji prostudovat některou z takto odborně zaměřených knih. Vhodným zdrojem informací je například [9].
4.1.1 Souřadnicový systém Direct3D pracuje s trojrozměrným souřadnicovým systémem. Proti 2D počítačové grafice, kde jsou objekty reprezentovány svoji šířkou a výškou, zde přibývá třetí rozměr – hloubka. Výhodou takového souřadnicového systému je, že odpovídá reálnému vnímání světa kolem nás. Z tohoto důvodu bývají aplikace založené na tomto systému názornější a realističtější. Abychom byli konkrétnější, Direct3D pracuje s kartézským pravoúhlým souřadnicovým systémem. To znamená, že veškerá poloha bodů je dána souřadnicemi na všech třech osách (x, y a z), které jsou pro kartézský systém charakteristické. Směr jednotlivých os v takovém systému se dá určit rukou, přičemž můžeme využít pravou i levou ruku. Podle toho, jakou ruku použijeme, liší se kladný směr osy z. Pravidlo levé ruky říká, že pokud tuto ruku nasměrujeme do kladného směru osy x a pokrčíme prsty (kromě palce) v kladném směru osy y, potom odkloněný palec udává kladný směr osy z. Stejné pravidlo platí i pro pravou ruku. Názorně je to uvedeno na obrázku 4.3. Direct3D standardně používá levoruký souřadnicový systém. Pokud se však provedou příslušné programové úpravy, je možné pracovat i s pravorukým souřadnicovým systémem. [14]
Kartézský souřadnicový systém Pravidlo levé ruky
y
y x
z
z x
4. Direct3D
Pravidlo pravé ruky
Obrázek 4.3: Pravoruký a levoruký kartézský souřadnicový systém
4.1.2 Vertexy, hrany a plochy Trojrozměrné objekty, které se prostřednictvím Direct3D zobrazují, bývají typu mesh. Anglické slovo mesh znamená v překladu síť, která definuje tvar objektu. Každá síť se přitom skládá z vertexů, hran a ploch. To jsou základní stavební kameny každého objektu a jejich kombinací lze docílit vzhledu libovolného tvaru. Vertex je obyčejný bod v prostoru (je jednoznačně dán svými souřadnicemi). Česky bychom mohli toto slovo přeložit jako vrchol, ačkoliv v 3D grafických programech, ve kterých se vytváří modely pro umísťování do aplikací, je možné vertexy vkládat na libovolné místo objektu. Na druhou stranu je ovšem žádoucí, aby vertexů bylo co nejméně, protože
4.1 Základní pojmy a transformace
Prodáno 14.10.2014 na www.Kosmas.cz zákazníkovi [email protected], číslo objednávky 591522, UID: 7e15c933-e4b8-4e2a-94a6-c3fd2dffdec9
78
DIRECTX
Obrázek 4.4: Ukázka 3D modelu stolu v drátovém zobrazení
jinak zbytečně zabírají místo v paměti a zpomalují průběh grafických algoritmů, které s nimi pracují. Pokud si tedy vezmeme obyčejnou krychli, ta by mohla být reprezentována osmi vertexy – v každém vrcholu jeden. Kromě souřadnic může vertex obsahovat i nějaké další informace. Těmito informacemi může být například barva, texturové souřadnice (tedy jaká část textury odpovídá tomuto bodu) nebo údaje o normálovém vektoru. Normálový vektor je velice důležitý prvek, který hraje dominantní roli při výpočtech stínování. Hrana představuje úsečku, tedy spojnici dvou vertexů. Pokud se vrátíme k příkladu krychle, zde budeme mít k osmi vertexům celkem dvanáct hran. Úplně první 3D aplikace či počítačové hry z důvodů malé kapacity paměti a nízké výpočetní rychlosti si s vertexy a hranami vystačily (například legendární hra Elite na počítači ZX Spectrum). Takovým modelům se říká drátové. Jejich výhodou jsou již zmíněné nízké paměťové nároky a rychlost zobrazování takových objektů. Naopak nevýhodou je nejednoznačnost (u některých modelů se těžko odhaduje, jaký mají skutečný tvar) a také nepříliš dobrý vizuální dojem. Z těchto důvodů jsou vertexy a hrany ještě doplněny plochami. V počítačové grafice se takové plochy nazývají polygony (mnohoúhelníky). Polygony bývají buď trojúhelníkové, nebo čtyřúhelníkové. Nejsnadněji se ovšem pracuje s trojúhelníky, protože jsou vždy rovinné. Každá z trojúhelníkových ploch je tak dána třemi vrcholy, které jsou spojeny celkem třemi hranami. Z většího počtu trojúhelníkových ploch se potom skládají složitější objekty. Ukázka takového objektu je na obrázku 4.4. Aby byly vidět jednotlivé vertexy, je model zobrazen pouze v drátové reprezentaci, tedy nejsou zobrazeny plochy.
4.1.3 Transformace a matice Transformace jsou často prováděné matematické operace. V Direct3D se setkáme s transformacemi dvojího druhu – základní transformace (posun, otáčení, změna měřítka) a transformace pro zobrazování objektů. Zobrazování trojrozměrných objektů se provádí prostřednictvím transformací lokálních souřadnic na globální, aplikací pohledové transformace a dále transformace promítání (projekce). Toto vše udává způsob zobrazení trojrozměrné scény. Zmíněné transformace si poprvé v programu vyzkoušíme v kapitole 4.7.
4. Direct3D
Prodáno 14.10.2014 na www.Kosmas.cz zákazníkovi [email protected], číslo objednávky 591522, UID: 7e15c933-e4b8-4e2a-94a6-c3fd2dffdec9
DIRECTX
79
Transformace se obvykle aplikuje pouze na vertexy. Pokud však máme nějakou komplexní scénu obsahující desítky či stovky různě složitých objektů, můžeme se klidně dostat i ke statisícům vertexů. Pro zjednodušení a urychlení výpočtů transformací je tak často výhodné používat transformační matice. Zde se dostáváme do teorie matematiky. Nepůjdeme však příliš do hloubky. Tyto informace je možné nalézt v každé „lepší“ matematické publikaci, proto si opět vybereme jen to nejpodstatnější. Matice je vlastně uspořádaná skupina čísel. Počet těchto čísel určuje velikost matice. Obecně může mít matice různý počet řádků a sloupců, my se ale budeme spíše setkávat s maticemi rozměru 4 × 4. Je to možná na první pohled poněkud nelogické, protože v prostoru máme trojici souřadnic, takže by se měla logicky používat matice 3 × 3. Podle zavedených standardů představuje čtvrtá hodnota tzv. váhu bodu, volí se obvykle 1 (viz [9]). S touto hodnotou budeme často pracovat i my, takže se dostáváme k rozměrům matice 4 × 4. Potom celý transformační vzorec vypadá následovně: M11 [x', y', z', 1] = [x, y, z, 1] × M21 M31 M41
M12 M22 M32 M42
M13 M23 M33 M43
M14 M24 M34 M44
Konečné hodnoty polohového vektoru tedy získáme násobením původních hodnot transformační maticí. Pokud si vezmeme konkrétní transformace, posun bude vypadat takto: 1 0 [x', y', z', 1] = [x, y, z, 1] × 0 1 0 0 XT YT
0 0 1 ZT
0 0 0 1
XT, YT a ZT znamenají úseky v jednotlivých osách, o které se bude původní bod posunovat. Transformace změny měřítka můžeme vyjádřit tímto vzorcem: 0 0 ZS 0
0 0 0 1
4. Direct3D
XS 0 [x', y', z', 1] = [x, y, z, 1] × 0 YS 0 0 0 0
XS, YS a ZS znamenají změnu měřítka v jednotlivých osách. Podle toho, zda budou tyto koeficienty větší nebo menší než 1, bude docházet v příslušném směru ke zvětšení nebo zmenšení. Transformace otáčení jsme si nechali na konec. Úmyslně je předchozí věta napsána v množném čísle, protože máme celkem tři možnosti – otáčení kolem jednotlivých os. Pokaždé se vzorce pochopitelně liší. Ve všech třech níže uvedených vzorcích představuje α úhel, o který se otáčení provádí. Otáčení kolem osy x: . 1 0 0 0 cos α sin α [x', y', z', 1] = [x, y, z, 1] × 0 -sin α cos α 0 0 0
0 0 0 1
4.1 Základní pojmy a transformace
Prodáno 14.10.2014 na www.Kosmas.cz zákazníkovi [email protected], číslo objednávky 591522, UID: 7e15c933-e4b8-4e2a-94a6-c3fd2dffdec9
80
DIRECTX
Otáčení kolem osy y: . cos α [x', y', z', 1] = [x, y, z, 1] × 0 sin α 0
0 -sin α 0 1 0 0 0 cos α 0 0 0 1
Otáčení kolem osy z: . cos α sin α 0 0 -sin α cos α 0 0 [x', y', z', 1] = [x, y, z, 1] × 0 0 1 0 0 0 0 1 Poslední informace, které si na tomto místě uvedeme, se týkají skládání transformací. Zde je opět výhoda matic, protože stačí mezi sebou vynásobit příslušné transformační matice. Jediné, na co je třeba dát pozor, je pořadí násobení matic. To musí odpovídat požadovanému pořadí transformací. Pokud například nějaký bod posuneme a potom otočíme, dostaneme jiné hodnoty, než kdybychom pořadí obou operací zaměnili.
4.2 Jednoduchá Direct3D aplikace v okně Dostáváme se k prvnímu programu, který bude založený na Direct3D. Tato aplikace bude běžet v okně (celoobrazovkový režim si popíšeme v následujících podkapitolách). Aby se něco provádělo, budeme měnit barvu pozadí. Na začátku vyplníme pozadí černou barvou, která se bude postupně zesvětlovat až na čistě bílou barvu. Potom se opět pozadí vyplní černou barvou a celý proces začne od začátku. Toto se bude cyklicky opakovat až do ukončení celého programu. Vyjdeme z Win32 aplikace, kterou jsme vytvořili v podkapitole 2.2. Programový kód podle potřeby upravíme a tyto úpravy si popíšeme. Stejně jako v čisté Win32 aplikaci budeme mít celkem šest souborů, jejichž názvy pro jednoduchost ponecháme beze změny. Začneme souborem Global.h, který obsahuje globální vlastnosti celé aplikace. Ten bude nyní vypadat následovně: #define _CRT_SECURE_NO_DEPRECATE 1 #pragma once #include <windows.h> #include <stdio.h> #include #pragma comment (lib,"dxguid.lib") #pragma comment (lib,"d3d9.lib") #define APP_NAME TEXT("Direct3D aplikace") #define WAITING_FOR_FLIP
1
#define RELEASE_DX_OBJECT(p)
{if(p){(p)->Release(); p = NULL;};};
extern TCHAR g_tcError[256];
4. Direct3D
Prodáno 14.10.2014 na www.Kosmas.cz zákazníkovi [email protected], číslo objednávky 591522, UID: 7e15c933-e4b8-4e2a-94a6-c3fd2dffdec9
DIRECTX
81
Na začátku souboru máme vložené makro _CRT_SECURE_NO_DEPRECATE s hodnotou 1. Toto makro je zcela zbytečné, pokud používáte kompilátor Visual C++ 6.0. Nicméně je vhodné ho implementovat do programů, které budou kompilovány v prostředí Visual C++ Express Edition. Zmíněná symbolická konstanta v tomto kompilátoru potlačuje varovné hlášení, které se týkají funkce sprintf. Budeme ji používat téměř v každém programu pro zápis případných chyb, které mohou vzniknout při běhu programu. Aby se varovná hlášení při kompilaci nevypisovala, je třeba, aby byla tato symbolická konstanta definována hned na začátku souboru, ještě před direktivou #pragma once. Potom vkládáme hlavičkové soubory windows.h a d3d9.h. Soubor d3d9.h budeme vkládat v každé aplikaci využívající Direct3D. Obsahuje vše potřebné, co k tvorbě takových aplikací potřebujeme. Je standardní součástí DirectX SDK a v kompilátoru je třeba k němu mít nastavenou cestu (viz podkapitolu 2.1). Potom následují dvě direktivy #pragma. Ty nám říkají, jaké knihovny budou k našemu projektu přilinkovány. Knihovnu dxguid.lib jsme používali již u DirectDraw aplikací, kde jsme si také uvedli, že poskytuje základní objekty a funkce pro všechny DirectX aplikace. V tomto programu být sice nutně nemusí, ale s ohledem na další rozšiřování programu ji zde můžeme ponechat. Nově zde přibyla knihovna d3d9.lib, která obsahuje rozhraní a jejich metody z Direct3D. Vložen je i hlavičkový soubor stdio.h kvůli funkci sprintf, kterou budeme používat v našem programu pro ukládání chybových hlášení (viz dále).
Na konci máme uvedenou deklaraci pole znaků s názvem g_tcError. V objektovém programování bychom se globálním deklaracím měli vyhýbat. K tomuto poli znaků ovšem potřebujeme přístup z různých programových modulů. Sem se bude ukládat text chyby, pokud při nějaké operaci vznikne. Deklaraci máme jako extern – skutečná deklarace i s počátečním naplněním se nachází v souboru WinMain.cpp.
4. Direct3D
Dále následuje trojice maker. Symbolická konstanta APP_NAME udává jméno naší aplikace a WAITING_FOR_FLIP udává hodnotu v milisekundách, jak často bude docházet k aktualizaci obsahu obrazovky. Aby cykly změny barev na pozadí netrvaly tak dlouho, je zde nastavena hodnota 1. Poslední makro je RELEASE_DX_OBJECT, které má jeden parametr. Tímto parametrem se rozumí nějaký objekt rozhraní DirectX. Metoda Release provádí uvolňování paměti. Po dealokaci paměti se ještě odehraje uvolnění příslušných ukazatelů příkazem p=NULL. Volání tohoto makra se provádí v okamžiku požadavku konce platnosti objektů – v našem případě to bude v okamžiku konce programu. Jakým způsobem se to provádí a pro které naše objekty se toto makro bude volat, uvidíme dále.
Právě soubor WinMain.cpp je další v pořadí, který si popíšeme. Ve srovnání s čistou Win32 aplikací jsme provedli podobné úpravy jako u DirectDraw. Přibyla nová globální proměnná unsigned int b_ActiveApp=TRUE. Do této proměnné budeme ukládat informace o tom, zda je naše okno aplikace aktivní (algoritmus programu bude probíhat jen tehdy, pokud je okno aplikace aktivní). Při spuštění aplikace je aktivní, proto je tato proměnná inicializována na TRUE. Zamezení běhu programu, pokud není aplikace aktivní, se dá provést v nekonečném cyklu při zpracovávání zpráv následovně: while(true) { if(PeekMessage(&msg, NULL, 0, 0, PM_REMOVE)) { if(msg.message == WM_QUIT) break; TranslateMessage(&msg);
4.2 Jednoduchá Direct3D aplikace v okně
Prodáno 14.10.2014 na www.Kosmas.cz zákazníkovi [email protected], číslo objednávky 591522, UID: 7e15c933-e4b8-4e2a-94a6-c3fd2dffdec9
82
DIRECTX
DispatchMessage(&msg); } if(b_ActiveApp) App.MainLoop(); } Máme tedy podmínku, že pokud bude mít proměnná b_ActiveApp nenulovou hodnotu, zavolá se metoda MainLoop instance App, která zajišťuje běh hlavní smyčky naší aplikace. Druhou (a zároveň poslední) globální deklaraci, kterou provádíme v souboru WinMain.cpp, je TCHAR g_tcError[256]. Jde o zmíněné pole znaků, do něhož se v případě chyby bude ukládat textový řetězec, který bude chybu popisovat. Preventivně do tohoto pole na začátku hlavní smyčky programu ukládáme textový řetězec "OK" příkazem sprintf(g_tcError,TEXT("OK"));. Pokud však při inicializaci k nějaké chybě přece jen dojde, ukončení programu a výpis chyby jsou zajištěny tak, že se celá aplikace ukončí (příkaz App. Terminate()) a zobrazí se dialogové okno s textem chyby: if(!App.Initialize()) { App.Terminate(); MessageBox (NULL, g_tcError, APP_NAME, MB_ICONERROR); return (0); } Když už jsme u volání metody App.Terminate();, tato metoda se volá i při ukončení programu, tedy bude to předposlední příkaz hlavní smyčky programu před return (msg.wParam);. Jejím cílem je provést uvolnění paměti od všech vytvořených objektů Direct3D. K bližšímu popisu této metody se ještě vrátíme v dalších částech této kapitoly. Poslední úprava souboru WinMain.cpp se týká funkce zpracovávající zprávy Windows. Z důvodu přehlednosti a jednodušší správy je tato funkce přesunuta do tohoto souboru (u čisté Win32 aplikace se nacházela v souboru CWindow.cpp). Tato funkce také prošla mírnou úpravou, proto ji tu uvedeme celou: LRESULT CALLBACK WndProc(HWND m_hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam) { switch(uMsg) { case WM_ACTIVATEAPP: b_ActiveApp = wParam; return (0); case WM_KEYDOWN: switch(wParam) { case VK_ESCAPE: PostQuitMessage(0); break; } return (0); case WM_DESTROY: PostQuitMessage(0); return (0); } return DefWindowProc(m_hWnd, uMsg, wParam, lParam); }
4. Direct3D
Prodáno 14.10.2014 na www.Kosmas.cz zákazníkovi [email protected], číslo objednávky 591522, UID: 7e15c933-e4b8-4e2a-94a6-c3fd2dffdec9
DIRECTX
83
Tato část kódu je úplně stejná, jako u DirectDraw aplikací. Celkem zpracováváme tři zprávy. WM_ACTIVATEAPP se generuje pokaždé, když okno aplikace přejde z aktivního stavu do neaktivního nebo naopak. Potom v parametru wParam zjistíme, o jakou změnu jde, a tento údaj uložíme do proměnné b_ActiveApp. Druhá zpráva (WM_KEYDOWN) se generuje v okamžiku stisknutí klávesy. Pokud se stiskne klávesa ESCAPE, program se ukončí. Třetí zpráva (WM_DESTROY) vznikne při standardním ukončování programu, tedy při stisknutí klávesové zkratky ALT+F4 nebo klepnutím na ikonu kříže v pravém horním rohu okna aplikace. Soubor CApplication.h tvoří deklarovaná třída CApplication. Té přibyla jedna nová metoda – MainLoop(void), která představuje hlavní smyčku běhu aplikace: #pragma once #include "CWindow.h" class CApplication { private: CWindow m_Window; long m_lPrevTick, m_lCurTick; public: CApplication(void); ~CApplication(void); bool Initialize(void); void Terminate(void); void MainLoop(void); void WriteText(void); Také máme (opět jako v aplikacích DirectDraw) přidány dvě nové soukromé proměnné m_lPrevTick a m_lCurTick. Do nich se bude ukládat počet milisekund od spuštění Windows. Důvodem je zjištění času, podle kterého se má provádět aktualizace obrazovky. Jak to vypadá konkrétně, zjistíme pohledem do souboru CApplication.cpp. Změn je ale více, proto si uvedeme tento soubor celý:
4. Direct3D
};
#pragma once #include "Global.h" #include "CApplication.h" CApplication::CApplication(void) { m_lPrevTick = 0; m_lCurTick = GetTickCount(); } CApplication::~CApplication(void) { m_Window.Terminate();
4.2 Jednoduchá Direct3D aplikace v okně
Prodáno 14.10.2014 na www.Kosmas.cz zákazníkovi [email protected], číslo objednávky 591522, UID: 7e15c933-e4b8-4e2a-94a6-c3fd2dffdec9
84
DIRECTX
} bool CApplication::Initialize(void) { if(!m_Window.Initialize()) return (false); return (true); } void CApplication::Terminate(void) { m_Window.Terminate(); } void CApplication::MainLoop(void) { m_lCurTick = GetTickCount(); if(m_lCurTick-m_lPrevTick > WAITING_FOR_FLIP) { m_lPrevTick = m_lCurTick; m_Window.UpdateFrame(); } } V konstruktoru této třídy nastavujeme soukromý atribut m_lPrevTick na hodnotu 0. Naproti tomu m_lCurTick naplníme hodnotou, kterou nám vrací funkce GetTickCount. Tato funkce Win32 API provádí to, co jsme si napsali výše – vrací počet milisekund od spuštění Windows. Účel těchto proměnných poodhalí metoda MainLoop. Zde běží smyčka programu a náš program má měnit barvu pozadí. To provádí metoda UpdateFrame, kterou si popíšeme dále v textu. Nyní nás ovšem zajímá, kdy se volá. Je to dáno podmínkou if(m_lCurTick-m_lPrevTick>WAITING_FOR_FLIP). Protože máme naše makro WAITING_FOR_FLIP nastaveno na hodnotu 1, pokud nám to rychlost počítače dovolí, bude se zmíněná metoda UpdateFrame volat tisíckrát každou vteřinu. Ostatní metody již nedělají příliš zajímavého. Destruktor může být prázdný, avšak z důvodu bezpečnosti zde voláme metodu Terminate instance m_Window. Tělo metody Initialize() je stejné jako u čisté Win32 aplikace. Zbylá metoda Terminate pouze volá stejnojmennou metodu instance m_Window. Zatím jsme v programu nenarazili na mnoho kódu z Direct3D. To se ale nyní změní, s popisem poslední dvojice souborů: CWindow.h a CWindow.cpp. Prvně jmenovaný soubor obsahuje deklaraci třídy CWindow: #pragma once class CWindow { private: HWND m_hWnd; LPDIRECT3D9 m_lpD3D; LPDIRECT3DDEVICE9 m_lpD3DDevice; short m_iColor; public:
4. Direct3D
Prodáno 14.10.2014 na www.Kosmas.cz zákazníkovi [email protected], číslo objednávky 591522, UID: 7e15c933-e4b8-4e2a-94a6-c3fd2dffdec9
DIRECTX
85
CWindow(void); ~CWindow(void); bool Initialize(void); void Terminate(void);
void UpdateFrame(void); friend LRESULT CALLBACK WndProc(HWND m_hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam); }; Přibyly nám tři atributy. Prvním z nich je m_lpD3D, což bude objekt rozhraní IDirect3D9, o kterém jsme si již řekli, že je základem každé Direct3D aplikace. Potom zde máme ještě atribut m_lpD3DDevice. Ten bude představovat zobrazovací zařízení (objekt rozhraní IDirect3DDevice9). V některých zdrojích můžete vidět obě instance deklarované zdánlivě odlišně: IDirect3D9 *m_lpD3D; IDirect3DDevice9 *m_lpD3DDevice; Oba tyto způsoby deklarace jsou však identické. Skutečnost je taková, že LPDIRECT3D9 představuje ukazatel na rozhraní IDirect3D9. Totéž platí i u druhého atributu.
Tělo třídy CWindow ještě tvoří hlavičky členských metod. Patří sem konstruktor, destruktor a metody Initialize, Terminate a UpdateFrame. Těla těchto metod jsou definována v souboru CWindow.cpp. Postupně si je uvedeme a okomentujme. Začneme konstruktorem: CWindow::CWindow(void) { m_hWnd = NULL; m_lpD3D = NULL; m_lpD3DDevice = NULL;
4. Direct3D
Posledním atributem je celočíselná hodnota m_iColor. Máme ji jako short, ve skutečnosti ale může nabývat hodnot 0–255, tedy pro její rozsah by stačil 1 byte. Do této proměnné budeme ukládat aktuální hodnotu barvy, která se bude pohybovat ve zmíněném rozmezí. Způsob změny tohoto atributu si popíšeme u funkce UpdateFrame, která je členskou metodou této třídy.
m_iColor=0; } Zde nalezneme počáteční inicializace všech členských atributů třídy. U prvních tří proměnných to provádíme z bezpečnostních důvodů, u proměnné m_iColor nastavujeme nulu, protože na začátku programu bude obrazovka černá a nula reprezentuje právě černou barvu. Pokud bychom chtěli tuto hodnotu hned na začátku nastavit jinak, nebyl by problém vytvořit parametrický konstruktor, který by se volal s parametrem kódu počáteční barvy. V destruktoru pouze voláme metodu CWindow::Terminate. Zde jsou těla obou metod: CWindow::~CWindow(void) {
4.2 Jednoduchá Direct3D aplikace v okně
Prodáno 14.10.2014 na www.Kosmas.cz zákazníkovi [email protected], číslo objednávky 591522, UID: 7e15c933-e4b8-4e2a-94a6-c3fd2dffdec9
86
DIRECTX
Terminate(); } void CWindow::Terminate(void) { RELEASE_DX_OBJECT(m_lpD3DDevice); RELEASE_DX_OBJECT(m_lpD3D); CloseWindow(m_hWnd); } V metodě Terminate dvakrát voláme makro s parametrem RELEASE_DX_OBJECT, které máme definováno v souboru Global.h. Těmito příkazy rušíme objekty dané ukazateli m_lpD3DDevice a m_lpD3D a uvolňujeme paměť, kterou měly tyto objekty přiděleny. Metoda Terminate se volá v okamžiku, kdy končí náš program (tuto metodu voláme i v proceduře zpracování zpráv Windows). Nakonec ještě příkazem CloseWindow okno aplikace zavíráme. Toto je potřeba především v případech, kdy program nečekaně skončí (například chyba při inicializaci), aby se na obrazovce mohlo objevit informační okno s textem vzniklé chyby. Metoda CWindow::Initialize je ze všech metod nejrozsáhlejší. Vytvoří okno aplikace, inicializuje Direct3D, vytvoří všechny potřebné objekty a zajistí první vykreslení. Její tělo vypadá následovně: bool CWindow::Initialize(void) { WNDCLASS wc; D3DPRESENT_PARAMETERS d3dpp; ZeroMemory(&wc, sizeof(wc)); wc.style = CS_HREDRAW|CS_VREDRAW; wc.lpfnWndProc = WndProc; wc.hInstance = GetModuleHandle(NULL); wc.hIcon = LoadIcon(NULL, IDI_WINLOGO); wc.hCursor = LoadCursor(NULL, IDC_ARROW); wc.hbrBackground = (HBRUSH) GetStockObject(WHITE_BRUSH); wc.lpszClassName = APP_NAME; if(!RegisterClass(&wc)) { sprintf(g_tcError,TEXT("Chyba při registraci okna aplikace!")); return (false); } if(!(m_hWnd = CreateWindowEx(0, APP_NAME, APP_NAME, WS_OVERLAPPEDWINDOW, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, NULL, NULL, wc.hInstance, NULL))) { sprintf(g_tcError,TEXT("Chyba při vytváření okna aplikace!")); return (false); }
4. Direct3D
Prodáno 14.10.2014 na www.Kosmas.cz zákazníkovi [email protected], číslo objednávky 591522, UID: 7e15c933-e4b8-4e2a-94a6-c3fd2dffdec9
DIRECTX
87
if(!(m_lpD3D = Direct3DCreate9(D3D_SDK_VERSION))) { sprintf(g_tcError,TEXT("Nelze vytvořit objekt Direct3D!")); return (false); } ZeroMemory(&d3dpp, sizeof(d3dpp)); d3dpp.Windowed = TRUE; d3dpp.SwapEffect = D3DSWAPEFFECT_DISCARD; d3dpp.BackBufferFormat = D3DFMT_UNKNOWN; if((m_lpD3D->CreateDevice(0, D3DDEVTYPE_HAL, m_hWnd, D3DCREATE_SOFTWARE_VERTEXPROCESSING, &d3dpp, &m_lpD3DDevice)) != D3D_OK) { sprintf(g_tcError,TEXT("Nelze vytvořit objekt Direct3DDevice!")); return (false); } if(m_lpD3DDevice) m_lpD3DDevice->Clear(0, NULL, D3DCLEAR_TARGET, D3DCOLOR_XRGB(0, 0, 0), 0, 0); else { sprintf(g_tcError,TEXT("Neexistuje objekt Direct3DDevice!")); return (false); }
ShowWindow(m_hWnd, SW_SHOW); UpdateWindow(m_hWnd);
4. Direct3D
m_lpD3DDevice->Present(NULL, NULL, NULL, NULL);
return (true); } Pokud se na tuto metodu podíváme trochu s nadhledem, zjistíme, že téměř všechny inicializační příkazy testujeme na návratovou hodnotu. Pokud něco neproběhlo v pořádku, příkazem sprintf uložíme text chyby do textového pole g_tcError a celá inicializační metoda končí neúspěchem, což poznáme podle vrácené hodnoty false. V celé knize jsou pro jednoduchost chyby ošetřovány tímto způsobem. Jednotlivé metody různých DirectX rozhraní vrací hodnotu typu HRESULT, ze které se dá kód chyby také vyčíst. Jak například v našem kódu vidíme, v případě, že je vše v pořádku, metody rozhraní Direct3D vrací obvykle hodnotu D3D_OK. Ve všech ostatních případech se vrací jiné hodnoty, které by měly usnadnit zjištění, k jaké chybě a proč došlo. Pokud byste chtěli ošetřovat chyby tímto způsobem, nahlédněte do dokumentace k DirectX SDK. Nyní si popišme tělo metody CWindow::Initialize podrobně. Začátek inicializace okna aplikace je téměř stejný jako u čisté Win32 aplikace. Tedy nejprve naplníme instanci struktury wc (typ WNDCLASS), kterou zaregistrujeme a vytvoříme okno zavoláním funkce CreateWindowEx.
4.2 Jednoduchá Direct3D aplikace v okně
Prodáno 14.10.2014 na www.Kosmas.cz zákazníkovi [email protected], číslo objednávky 591522, UID: 7e15c933-e4b8-4e2a-94a6-c3fd2dffdec9
88
DIRECTX
Po vytvoření okna vytvoříme objekt m_lpD3D. To provádí funkce Direct3DCreate9. Pokud by došlo k nějaké chybě (například DirectX 9 není v systému vůbec instalováno), vrací se nulová hodnota. Funkce Direct3DCreate9 má pouze jeden parametr, kterým je vždy D3D_SDK_VERSION. Jakmile máme vytvořený Direct3D objekt, jeho prostřednictvím můžeme vytvářet jiné objekty Direct3D rozhraní. V tomto programu si vystačíme pouze s jedním, a sice se zařízením, které umožňuje pracovat s grafickými zdroji, tedy umožňuje vykreslovat na obrazovku. Vytvoření takového zařízení provádí metoda IDirect3D9::CreateDevice. V případě úspěšného vytvoření se vrací hodnota D3D_OK. Pokud se vrací jakákoliv jiná hodnota, zařízení se vytvořit nepodařilo a v takovém případě se program ukončí. Ještě před voláním metody CreateDevice potřebujeme naplnit některé položky instance struktury d3dpp (struktura typu D3DPRESENT_PARAMETERS). Ukazatel na tuto strukturu je totiž jeden z parametrů metody IDirect3D9::CreateDevice. D3DPRESENT_PARAMETERS a četné množství položek této struktury udává důležité vlastnosti vytvářeného zařízení. Pokud vás zajímají všechny tyto prvky, nahlédněte do dokumentace u DirectX SDK. Téma je rozsáhlé, proto si tu popíšeme pouze ty položky, které nás aktuálně zajímají. Jako první všechny položky této struktury vynulujeme (funkce ZeroMemory). Poté nastavujeme tři položky této struktury. První z nich je d3dpp.Windowed, která očekává binární hodnotu. Zde nastavujeme, zda bude aplikace běžet v okně (TRUE) nebo v celoobrazovkovém režimu (FALSE). Druhý prvek d3dpp.SwapEffect udává způsob přenášení dat mezi front a back bufferem. Počet back bufferů mezi příkazy nenastavujeme, což znamená, že máme jeden back buffer. Zde si doplňme, že počet back bufferů můžete nastavit položkou d3dpp. BackBufferCount. Tím, že toto nenastavuje, máme zde hodnotu 0 (vynulování nám provedla zmíněná funkce ZeroMemory). Výše jsme si ale také uvedli, že Direct3D má vždy alespoň jeden back buffer, proto s hodnotou 0 Direct3D zachází, jako by to byla hodnota 1. Vraťme se ale k položce d3dpp.SwapEffect. Zde se prakticky používají hodnoty D3DSWAPEFFECT_DISCARD, D3DSWAPEFFECT_FLIP a D3DSWAPEFFECT_COPY. Hodnota D3DSWAPEFFECT_FLIP se dá i použít pro více back bufferů a v daných okamžicích dochází vždy k záměně front bufferu a jednoho back bufferu. Pokud je back bufferů více, tak se v pořadí cyklicky řadí do fronty, takže se při záměně obsahu s front bufferem všechny pravidelně střídají. Naproti tomu se D3DSWAPEFFECT_COPY používá pouze tehdy, pokud máme jen jeden back buffer a odtud se data přenášejí do front bufferu. Poslední parametr D3DSWAPEFFECT_DISCARD říká, že pro přenos obsahu back bufferu do front bufferu bude použit nejúčinnější algoritmus. Poslední parametr, který u instance d3dpp nastavujeme, je BackBufferFormat. Zde se zadává, jak to bude s barvami (respektive barevnou hloubkou) ve vytvářených bufferech. Tady je možností parametrů hodně, proto je tu všechny nebudeme popisovat. Některé si uvedeme v dalších aplikacích, kde je budeme používat. Naše současná aplikace běží v okně, a proto bude přejímat vlastnosti Windows. Z těchto důvodů zde máme přiřazenou symbolickou konstantu D3DFMT_UNKNOWN (doslova to znamená, že tato hodnota „není definována“). Nyní se vraťme k metodě IDirect3D9::CreateDevice. Její obecná syntaxe je následující: HRESULT IDirect3D9::CreateDevice( UINT Adapter, D3DDEVTYPE DeviceType, HWND hFocusWindow,
4. Direct3D
Prodáno 14.10.2014 na www.Kosmas.cz zákazníkovi [email protected], číslo objednávky 591522, UID: 7e15c933-e4b8-4e2a-94a6-c3fd2dffdec9
DIRECTX
89
DWORD BehaviorFlags, D3DPRESENT_PARAMETERS * pPresentationParameters, IDirect3DDevice9 ** ppReturnedDeviceInterface ); Parametrů této metody je tedy celkem šest. První (Adapter) představuje adaptér, pro který se má zařízení vytvořit. Zde se výlučně setkáváme s hodnotou 0 nebo identifikátorem D3DADAPTER_DEFAULT, který má přiřazenou hodnotu 0 a znamená to, že se používá standardní zobrazovací zařízení. Druhým parametrem v řade je DeviceType. To definuje typ objektu zařízení, které se má použít. Identifikátor v naší aplikaci D3DDEVTYPE_HAL říká, že pokud to půjde, operace se budou provádět hardwarově. Třetím argumentem je handle okna aplikace, k němuž bude zobrazovací zařízení vázáno. Čtvrtým parametrem jsou příznaky, které definují vlastnosti zařízení. V našem programu používáme příznak D3DCREATE_SOFTWARE_VERTEXPROCESSING, což znamená, že se budou vertexy zpracovávat softwarově. Pokud to umožňuje fyzické vybavení počítače, zpracování může probíhat i hardwarově – příznak D3DCREATE_HARDWARE_VERTEXPROCESSING, nebo hardwarově i softwarově, tedy kombinovaně – příznak D3DCREATE_MIXED_VERTEXPROCESSING. Pátý parametr je ukazatel na instanci struktury typu D3DPRESENT_PARAMETERS, kde jsou uloženy další vlastnosti zařízení. V našem programu je to d3dpp, jejíž nastavené hodnoty jsme si popsali výše. Posledním parametrem je adresa ukazatele na rozhraní IDirect3DDevice9, tedy objekt rozhraní, který vytváříme a skrze nějž budeme přistupovat k front bufferu a back bufferu. Po vytvoření zobrazovacího zařízení, front a back bufferu vymažeme back buffer černou barvou. To provedeme voláním metody Clear u tohoto zařízení. Volání této metody je podmíněno tím, že příslušné zařízení musí existovat. To by ale zcela jistě mělo, protože pokud by neexistovalo, u metody CreateDevice by se nevrátilo D3D_OK, další příkazy metody Initialize by se nevolaly a celý program by skončil. Jinými slovy by zde tato podmínka ani nemusela být.
HRESULT IDirect3DDevice9::Clear( DWORD Count, CONST D3DRECT * pRects, DWORD Flags, D3DCOLOR Color, float Z, DWORD Stencil );
4. Direct3D
Metoda IDirect3DDevice9::Clear má celkem šest parametrů:
První dva parametry společně úzce souvisejí. Druhý parametr je ukazatel na pole obdélníkových ploch (pole struktur typu D3DRECT), které jsou dány svými souřadnicemi. První parametr udává, kolik se má těchto obdélníků vymazat. V našem programu máme první hodnotu nastavenou na 0 a druhá je NULL (ukazatel směřuje „nikam“). Tím říkáme, že budeme pracovat s celou obrazovkou. Třetím parametrem může být kombinace příznaků, kterými nastavujeme, co se má vymazat. D3DCLEAR_TARGET znamená, že se bude mazat back buffer, tedy místo v paměti: vykreslujeme to, co se později objeví na obrazovce. Kromě tohoto zde může být ještě příznak D3DCLEAR_STENCIL nebo D3DCLEAR_ZBUFFER pro vymazání stencil bufferu či z-bufferu. Z-buffer je hloubkový, tedy uchovává informace o vzdálenostech z pohledu kamery směrem do scény a využívá se například pro zjištění toho, co se má vlastně vykreslovat (vzdálené objekty nebo jejich části mohou být zakryté těmi, které jsou blíže kameře, a tak je zbytečné je vůbec vykreslovat na obrazovku).
4.2 Jednoduchá Direct3D aplikace v okně
Prodáno 14.10.2014 na www.Kosmas.cz zákazníkovi [email protected], číslo objednávky 591522, UID: 7e15c933-e4b8-4e2a-94a6-c3fd2dffdec9
90
DIRECTX
Stencil buffer se používá především k maskování, tedy uchovává pomocné hodnoty, které určují, zda se mají určité pixely vykreslovat. Čtvrtým parametrem je barva, kterou vyplňujeme. Je ve formátu ARGB. D3DCOLOR_XRGB(0, 0, 0) znamená v barevném modelu RGB černou barvu. Předposlední hodnota je neceločíselná a pohybuje se v rozmezí 0–1. Smysl má pouze tehdy, pokud se maže z-buffer, který představuje „hloubku“, čili vlastně třetí rozměr. A právě rozsah této hloubky udává tento parametr. Protože zde se z-bufferem nepracujeme, je v našem programu hodnota 0. Podobně poslední hodnota udává rozsah mazání ve stencil bufferu. Zde se však neuvádí neceločíselná hodnota, ale číslo n, které udává rozsah 0–2n-1 (bitová hloubka). Protože ani se stencil bufferem nepracujeme, máme v naší aplikaci dosazenou hodnotu 0. Posledním příkazem v metodě Initialize je m_lpD3DDevice->Present. V našem programu toto volání způsobí záměnu obsahů front a back bufferu, tedy to, co je v back bufferu a co se objeví na obrazovce. Pro provedení této činnosti stačí všechny čtyři parametry metody Present nastavit na hodnoty NULL. Obecně však tato metoda udává, co se kam a jak má přenést. Protože však i v dalších programech budeme výlučně používat všechny čtyři parametry jako NULL, pokud se chcete přesně dozvědět, co který parametr znamená, nahlédněte do dokumentace DirectX SDK. Poslední metodou třídy CWindow, kterou jsme si dosud nepospali, je UpdateFrame: void CWindow::UpdateFrame(void) { if(m_lpD3DDevice) { m_iColor = (m_iColor == 255) ? 0 : ++m_iColor; m_lpD3DDevice->Clear(0, NULL, D3DCLEAR_TARGET, D3DCOLOR_XRGB(m_iColor, m_iColor, m_iColor), 0, 0); m_lpD3DDevice->Present(NULL, NULL, NULL, NULL); } } Tato metoda se volá v okamžiku, kdy se má provést aktualizace obsahu obrazovky. V naší aplikaci se mění jen barva pozadí. Barva pozadí se mění od černé D3DCOLOR_XRGB(0, 0, 0) po čistě bílou D3DCOLOR_XRGB(255, 255, 255). Hodnotu barvy máme uloženou ve členské proměnné m_iColor, která se každým cyklem inkrementuje. Jakmile dosáhne svoji maximální hodnoty, tedy 255, nastaví se opět na nulu. Výplň pozadí definovanou barvou v back bufferu provádí metoda Clear a vykreslení na obrazovku zajistí metoda Present. Obě tyto metody jsme si popsali výše.
4.3 Direct3D v celoobrazovkovém režimu Jednoduchá Direct3D aplikace popsaná v předchozí kapitole běžela v okně. V celoobrazovkovém režimu bude taková aplikace vytvořená poněkud odlišně. Jak to přesně vypadá, uvidíme v této podkapitole. Na tomto místě se zaměříme na jednu zajímavou a důležitou věc, která se aplikace v celoobrazovkovém režimu týká. Jde o rozlišení obrazu. Jak jistě víte, rozlišení obrazu nemůžeme nastavit libovolně, ale jsou zavedeny určité standardy, které ovšem nemusí být na různých počítačích vždy dostupné, neboť závisí na jejich hardwarovém vybavení. Nyní si tedy ukážeme a popíšeme aplikaci, která zjistí, jaké rozlišení a barevné hloubky si můžeme nastavit.
4. Direct3D
Prodáno 14.10.2014 na www.Kosmas.cz zákazníkovi [email protected], číslo objednávky 591522, UID: 7e15c933-e4b8-4e2a-94a6-c3fd2dffdec9
DIRECTX
91
Protože se spokojíme pouze s těmito informacemi, aplikace nemusí vytvářet okno a testovat zprávy Windows zasílané naší aplikaci. Program pouze proběhne, zjistí všechna rozlišení a poté zase skončí. Pro jednoduchost zapíšeme zjištěné informace do textového souboru. Celý program máme uložen ve dvou souborech. První z nich je soubor Global.h: #pragma once #include <windows.h> #include #include #pragma comment (lib,"d3d9.lib") #define TITLE_TEXT TEXT("Zjištění podporovaných rozlišení v Direct3D") #define RESOLUTION_FILE TEXT("Rozliseni.txt") #define RELEASE_DX_OBJECT(p)
{if(p){(p)->Release(); p=NULL;};};
Nově je vložený hlavičkový soubor fstream, který obsahuje vše potřebné pro práci se soubory. Potom zde ještě máme symbolické konstanty TITLE_TEXT a RESOLUTION_FILE. Obě makra představují textový řetězec. V prvním případě je to text titulku informačního okna aplikace, které zobrazíme na obrazovku. Druhé makro má v sobě uloženo jméno souboru, do kterého se budou dostupná rozlišení ukládat. Soubor WinMain.cpp vypadá takto: #include "Global.h"
int WINAPI WinMain(HINSTANCE, HINSTANCE, LPSTR, int) { fstream F; LPDIRECT3D9 lpD3D = NULL; D3DDISPLAYMODE D3DDm; D3DFORMAT D3DFormat = D3DFMT_X8R8G8B8;
4. Direct3D
using namespace std;
if(lpD3D = Direct3DCreate9(D3D_SDK_VERSION)) { int i_allVideoModes = lpD3D->GetAdapterModeCount (D3DADAPTER_DEFAULT, D3DFormat); if(i_allVideoModes == 0) { MessageBox(NULL, TEXT("Žádné rozlišení pro vybranou barevnou hloubku není podporováno !"), TITLE_TEXT, MB_OK); } else {
4.3 Direct3D v celoobrazovkovém režimu
Prodáno 14.10.2014 na www.Kosmas.cz zákazníkovi [email protected], číslo objednávky 591522, UID: 7e15c933-e4b8-4e2a-94a6-c3fd2dffdec9
92
DIRECTX
F.open(RESOLUTION_FILE, ios::out|ios::trunc); F<<TITLE_TEXT<<" pro vybranou barevnou hloubku"<<endl; for(int i = 0; i < i_allVideoModes; i++) { lpD3D->EnumAdapterModes (D3DADAPTER_DEFAULT, D3DFormat, i, &D3DDm); F<
4. Direct3D
Prodáno 14.10.2014 na www.Kosmas.cz zákazníkovi [email protected], číslo objednávky 591522, UID: 7e15c933-e4b8-4e2a-94a6-c3fd2dffdec9
DIRECTX
93
8 bitů je nevyužito. Naproti tomu D3DFMT_R5G6B5 představuje 16bitovou barevnou hloubku (tedy 216, 65 536 barev), kdy má každá barva 5 bitů a poslední bit je nevyužitý. V programu dále následuje známá část z předchozí kapitoly – vytváříme Direct3D objekt voláním funkce Direct3DCreate9. Získaný objekt potom využíváme k volání metody IDirect3D::GetAdapterModeCount. Tato metoda vrací dostupný počet rozlišení. Tento počet si ukládáme do proměnné i_allVideoModes. Parametry metody IDirect3D::GetAdapterModeCount jsou adaptér, pro který rozlišení zjišťujeme – tedy standardní zobrazovací zařízení (D3DADAPTER_DEFAULT) a barevná hloubka, která je položkou z výčtu D3DFORMAT. Pokud by náhodou žádné rozlišení obrazovky pro takovou barevnou hloubku neexistovalo, vypíše se příslušný text ve formě informačního okna (MessageBox) na obrazovku a program skončí. V opačném případě se budou dostupná rozlišení zjišťovat a zapisovat do souboru. Pro zjištění rozlišení používáme cyklus, který začínáme hodnotou 0 a končíme hodnotou i_allVideoModes-1. V každém cyklu voláme skrze metodu IDirect3D::EnumAdapterModes. Tato metoda zjistí na základě nastavené čtveřice parametrů všechny potřebné informace. Prvním parametrem metody IDirect3D::EnumAdapterModes je opět adaptér, pro který se informace zjišťují, tedy D3DADAPTER_DEFAULT. Druhým parametrem je barevná hloubka ve formátu D3DFORMAT. Třetím parametrem je index režimu. Sem dosazujeme proměnnou hodnotu cyklu. Konečně posledním parametrem je ukazatel na strukturu typu D3DDISPLAYMODE, jejíž položky se po úspěšném zavolání této metody naplní. V dalších krocích přistupujeme k položkám instance této struktury D3DDm – Width a Height, odkud rozlišení zapisujeme do souboru.
Zjištění podporovaných rozlišení v Direct3D pro vybranou barevnou hloubku Šířka: 320, Výška: 200 Šířka: 320, Výška: 240 Šířka: 400, Výška: 300 Šířka: 512, Výška: 384 Šířka: 640, Výška: 400 Šířka: 640, Výška: 480 Šířka: 720, Výška: 480 Šířka: 720, Výška: 576 Šířka: 720, Výška: 576 Šířka: 800, Výška: 600 Šířka: 848, Výška: 480 Šířka: 1024, Výška: 480 Šířka: 1024, Výška: 768 Šířka: 1152, Výška: 864 Šířka: 1280, Výška: 720 Šířka: 1280, Výška: 768 Šířka: 1280, Výška: 960 Šířka: 1280, Výška: 1024
4. Direct3D
Na závěr se podívejme na výstup z programu. Jde o výpis souboru Rozliseni.txt pro grafickou kartu ATI RADEON 9600, kterou mám ve svém počítači (barevná hloubka byla nastavena jako D3DFMT_X8R8G8B8):
4.3 Direct3D v celoobrazovkovém režimu
Prodáno 14.10.2014 na www.Kosmas.cz zákazníkovi [email protected], číslo objednávky 591522, UID: 7e15c933-e4b8-4e2a-94a6-c3fd2dffdec9
94
DIRECTX
4.4 Surfaces v Direct3D V podkapitole 3.4 jsme popisovali tzv. off-screen surfaces, do kterých se ukládají obrázky. Celé tyto obrázky nebo jejich části se potom mohly objevovat na obrazovce. V této podkapitole si předvedeme totéž, ale s použitím Direct3D. Popíšeme si všechny funkce a metody, které pro práci s nimi potřebujeme, tedy vytvoření surface, načtení obrázku na něj a jeho přenos do back bufferu. Příklad bude jednoduchý. Vytvoříme celoobrazovkovou aplikaci, do které načteme jeden obrázek ze souboru a zobrazíme ho na obrazovku. Při té příležitosti si také ukážeme, jak vyřešit problém se ztrátou zobrazovacího zařízení. Vyjdeme ze šestice souborů, jejichž obsahy budou odpovídat programu popsanému v podkapitole 4.2. a jako obvykle se zmíníme o úpravách, které implementujeme nově. Jako obvykle začneme souborem Global.h: #define _CRT_SECURE_NO_DEPRECATE 1 #pragma once #include #include #include #include
<windows.h> <stdio.h>
#pragma comment (lib,"dxguid.lib") #pragma comment (lib,"d3d9.lib") #pragma comment (lib,"d3dx9.lib")
#define APP_NAME #define FILEIMAGE
TEXT("Direct3D aplikace") TEXT("Image.jpg")
#define SCREEN_WIDTH
640
#define SCREEN_HEIGHT #define SCREEN_DEPTH
480 32
#define WAITING_FOR_FLIP
100
#define RELEASE_DX_OBJECT(p)
{if(p){(p)->Release(); p = NULL;};};
extern TCHAR g_tcError[256]; Jak můžeme vidět, soubor Global.h obsahuje několik novinek. Na začátku si můžeme všimnout nově vloženého hlavičkového souboru d3dx9.h a odpovídající přilinkovávané knihovny d3dx9.lib. Důvodem vložení je funkce D3DXLoadSurfaceFromFile, která je ve jmenovaných souborech definována. Tato funkce načítá obrázek ze souboru a ukládá ho do daného surface. Blíže si ji popíšeme v následujících odstavcích. Dále přibyla čtveřice nových symbolických konstant. Makro FILEIMAGE obsahuje název souboru, ze kterého se bude obrázek načítat, a symbolické konstanty SCREEN_WIDTH, SCREEN_HEIGHT a SCREEN_DEPTH udávající šířku obrazovky, výšku obrazovky a barevnou hloubku.
4. Direct3D
Prodáno 14.10.2014 na www.Kosmas.cz zákazníkovi [email protected], číslo objednávky 591522, UID: 7e15c933-e4b8-4e2a-94a6-c3fd2dffdec9
DIRECTX
95
V souboru WinMain.cpp dojde k jediné změně. Tato změna se týká možné chyby, která vznikne za běhu programu. Dříve jsme chyby a případné ukončení testovali pouze u inicializace celé aplikace. Nyní je však program rozsáhlejší, proto ošetřujeme i možné chyby, které vzniknou za běhu programu (poté, co inicializace již skončila). Pokud by k nějaké takové chybě došlo, aplikace se ukončí a my budeme chtít zobrazit dialogové okno s chybou, ke které došlo. Abychom ošetřili, že se toto okno objeví pouze při nečekané chybě a ne po každém ukončení aplikace, na konec funkce WinMain (těsně před příkaz return (msg.wParam)) vložíme kód: if(lstrcmp(g_tcError, TEXT("OK"))) { MessageBox (NULL, g_tcError, APP_NAME, MB_ICONERROR); return (0); } Podmínka je jednoduchá. Funkce lstrcmp porovnává dva řetězce a pokud jsou shodné (tedy program končí plánovaně), vrací se hodnota 0 a tělo podmínky se přeskočí. V opačném případě se zobrazí dialogové okno se zprávou, která bude popisovat vzniklou chybu. V souboru CApplication.h žádné úpravy provádět nebudeme. V tělech metod třídy CApplication (soubor CApplication.cpp) provedeme dvě změny. První změna se týká těla metody Initialize:
Místo volání jedné metody CWindow::Initialize voláme metody dvě. Tyto dvě metody (CWindow::InitializeWindow a CWindow::InitializeDirect3D()) vznikly rozdělením původní metody CWindow::Initialize. První z nich vytváří okno aplikace pomocí standardních funkcí Windows a druhá pak všechny počáteční inicializace, které se týkají Direct3D. Důvodem tohoto rozdělení je nová inicializace Direct3D při ztrátě zařízení (viz další odstavce).
4. Direct3D
bool CApplication::Initialize(void) { if(!m_Window.InitializeWindow()) return (false); if(!m_Window.InitializeDirect3D()) return (false); return (true); }
Druhá změna se nachází v metodě CApplication::Terminate: void CApplication::Terminate(void) { m_Window.Terminate(); HWND hwnd = m_Window.GetHWnd(); CloseWindow(hwnd); } Tato metoda se, jak víme, volá na konci aplikace. Kromě zrušení všech objektů Direct3D zjišťujeme handle okna aplikace (jak uvidíme dále, třída CWindow obsahuje mimo jiné i novou metodu GetHWnd, která toto handle vrací) a na základě tohoto handle se zavírá okno aplikace. Dříve jsme okno nezavírali. Proč to děláme nyní? Je to opět kvůli možné chybě, která vznikne za běhu programu a aplikaci ukončí. Okno zavíráme, abychom při
4.4 Surfaces v Direct3D
Prodáno 14.10.2014 na www.Kosmas.cz zákazníkovi [email protected], číslo objednávky 591522, UID: 7e15c933-e4b8-4e2a-94a6-c3fd2dffdec9
96
DIRECTX
takovém neplánovaném ukončení programu viděli informační okno s chybou (v opačném případě by bylo překryté oknem aplikace). Nyní si popišme změny, jimiž projde třída CWindow (soubor CWindow.cpp). Tělo této třídy vypadá nyní takto: class CWindow { private: HWND m_hWnd; LPDIRECT3D9 m_lpD3D; LPDIRECT3DDEVICE9 m_lpD3DDevice; LPDIRECT3DSURFACE9 m_lpBackImage; public: CWindow(void); ~CWindow(void); bool bool void void
InitializeWindow(void); InitializeDirect3D(void); Restore(); Terminate(void);
HWND GetHWnd(void) const{return (m_hWnd);} void UpdateFrame(void); friend LRESULT CALLBACK WndProc(HWND m_hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam); }; Proti aplikaci popsané v podkapitole 4.2 je odstraněn atribut m_iColor, který udával barvu pozadí. Místo něj naopak přibyl m_lpBackImage, což je ukazatel na rozhraní IDirect3D9Surface9, tedy objekt, který bude reprezentovat načtený obrázek. Kromě zmíněných metod GetHWnd (protože pouze vrací handle, je tato metoda ve třídě přímo i definována), InitializeWindow a InitializeDirect3D přibyla ještě jedna metoda: Restore, která se bude starat o obnovu ztraceného zařízení. Jako obvykle máme v souboru CWindow.cpp definovány těla metod třídy CWindow. V konstruktoru opět inicializujeme všechny atributy třídy CWindow: CWindow::CWindow(void) { m_hWnd = NULL; m_lpD3D = NULL; m_lpD3DDevice = NULL; m_lpBackImage = NULL; } V destruktoru pouze voláme metodu CWindow:Terminate, která po atributech třídy „uklízí“: void CWindow::Terminate(void) {
4. Direct3D
Prodáno 14.10.2014 na www.Kosmas.cz zákazníkovi [email protected], číslo objednávky 591522, UID: 7e15c933-e4b8-4e2a-94a6-c3fd2dffdec9
DIRECTX
97
RELEASE_DX_OBJECT(m_lpBackImage); RELEASE_DX_OBJECT(m_lpD3DDevice); RELEASE_DX_OBJECT(m_lpD3D); } Z důvodu volání této metody při opětovné inicializaci si můžeme všimnout, že proti programu popsanému v podkapitole 4.2 chybí volání funkce CloseWindow a jak víme, její volání bylo přesunuto do metody CWindow::Terminate třídy CApplication. Metoda CWindow::InitializeWindow obsahuje běžné příkazy vytvoření okna aplikace, které jsme si popsali již v podkapitole 2.2, tedy registraci třídy okna a vytvoření okna aplikace. Protože však vytváříme celoobrazovkovou aplikaci, při vytváření okna funkcí CreateWindowEx musíme nastavit čtvrtý parametr této funkce na WS_OVERLAPPEDWINDOW (to jsme prováděli i u celoobrazovkových DirectDraw aplikací). Jiné změny zde nemáme, proto tento programový úsek nebudeme opětovně uvádět. Naopak více změn potkalo metodu CWindow::InitializeDirect3D: bool CWindow::InitializeDirect3D(void) { D3DPRESENT_PARAMETERS d3dpp; D3DFORMAT d3dFormat; d3dFormat = (SCREEN_WIDTH == 32) ? D3DFMT_X8R8G8B8 : D3DFMT_R5G6B5;
ZeroMemory(&d3dpp, sizeof(d3dpp)); d3dpp.Windowed = false; d3dpp.SwapEffect = D3DSWAPEFFECT_DISCARD; d3dpp.BackBufferFormat = d3dFormat; d3dpp.BackBufferWidth = SCREEN_WIDTH; d3dpp.BackBufferHeight = SCREEN_HEIGHT; d3dpp.hDeviceWindow = m_hWnd;
4. Direct3D
if(!(m_lpD3D = Direct3DCreate9(D3D_SDK_VERSION))) { sprintf(g_tcError,TEXT("Nelze vytvořit objekt Direct3D!")); return (false); }
if((m_lpD3D->CreateDevice(0, D3DDEVTYPE_HAL, m_hWnd, D3DCREATE_SOFTWARE_VERTEXPROCESSING, &d3dpp, &m_lpD3DDevice)) != D3D_OK) { sprintf(g_tcError,TEXT("Nelze vytvořit objekt Direct3DDevice!")); return (false); } if(m_lpD3DDevice) m_lpD3DDevice->Clear(0, NULL, D3DCLEAR_TARGET, D3DCOLOR_XRGB(0, 0, 0), 0, 0); else {
4.4 Surfaces v Direct3D
Prodáno 14.10.2014 na www.Kosmas.cz zákazníkovi [email protected], číslo objednávky 591522, UID: 7e15c933-e4b8-4e2a-94a6-c3fd2dffdec9
98
DIRECTX
sprintf(g_tcError,TEXT("Neexistuje objekt Direct3DDevice!")); return (false); } m_lpD3DDevice->Present(NULL, NULL, NULL, NULL); if((m_lpD3DDevice->CreateOffscreenPlainSurface(SCREEN_WIDTH, SCREEN_HEIGHT, d3dFormat, D3DPOOL_DEFAULT, &m_lpBackImage, NULL)) != D3D_OK) { sprintf(g_tcError,TEXT("Nelze vytvořit off-screen surface!")); return (false); } if((D3DXLoadSurfaceFromFile(m_lpBackImage, NULL, NULL, FILEIMAGE, NULL, D3DX_DEFAULT, 0, NULL)) != D3D_OK) { sprintf(g_tcError,TEXT("Nelze načíst obrázek ze souboru!")); return (false); } return (true); } Na začátku máme deklarovány dva identifikátory datových typů D3DPRESENT_PARAMETERS a D3DFORMAT. S oběma jsme se již setkali v předchozích podkapitolách. První představuje strukturu, která obsahuje položky, do nichž se musí vyplnit vlastnosti vytvářeného zařízení. Jedna z nich se týká nastavení barevné hloubky (konkrétně jde o d3dpp.BackBufferFormat). Sem ovšem nemůžeme dosadit jednoduše barevnou hloubku v bitech, tedy 16 nebo 32, ale právě identifikátor typu D3DFORMAT, který jsme si popsali v předchozí kapitole. Všechny formáty a jejich identifikátory se dají najít v DirectX SDK, proto jsme si uvedli jen dva nejčastější (D3DFMT_X8R8G8B8 a D3DFMT_R5G6B5), které nastavujeme právě podle makra SCREEN_WIDTH příkazem: d3dFormat = (SCREEN_WIDTH == 32) ? D3DFMT_X8R8G8B8 : D3DFMT_R5G6B5; Dále funkcí Direct3DCreate9 provedeme vytvoření objektu Direct3D a potom metodou IDirect3D9::CreateDevice vytváříme objekt zobrazovacího zařízení. Nové jsou ale pro nás některé položky struktury typu D3DPRESENT_PARAMETERS, jimiž nastavujeme vlastnosti tohoto zařízení. Již víme, že d3dpp.BackBufferFormat obsahuje identifikátor barevné hloubky. Také jsme si uvedli, že do d3dpp.Windowed ukládáme, zda má být okno aplikace přes celou obrazovku či nikoliv. Zde celoobrazovkovou aplikaci vytváříme, proto je zde nastavena hodnota false. Další položku jsme již také popsali – d3dpp.SwapEffect udává způsob přenášení dat mezi front a back bufferem. Máme pouze jeden back buffer, proto ani nemusíme nastavovat d3dpp.BackBufferCount a můžeme ponechat tuto položku na nule. Následují dvě položky: d3dpp.BackBufferWidth a d3dpp.BackBufferHeight udávají rozměry back bufferu, tedy vlastně i rozlišení obrazovky. U okenních aplikací jsme se o tyto dvě položky nemuseli zajímat, ale zde je to nutné. Do poslední položky (d3dpp.hDeviceWindow) ukládáme handle okna naší aplikace.
4. Direct3D
Prodáno 14.10.2014 na www.Kosmas.cz zákazníkovi [email protected], číslo objednávky 591522, UID: 7e15c933-e4b8-4e2a-94a6-c3fd2dffdec9
DIRECTX
99
Po vytvoření obrazovky se back buffer vyplní černou barvou a zobrazí se. Tato část kódu by tu pochopitelně být nemusela, protože hned v dalších krocích vytváříme off-screen surface a do něj načítáme obrázek. Potom se bude za běhu programu obrázek nahrávat do back bufferu, takže se tímto obrázkem celé černé pozadí překreslí. Vytváření off-screen surface provádí metoda IDirect3DDevice9::CreateOffscreenPlainSurface, jejíž syntaxe je následující: HRESULT IDirect3DDevice9::CreateOffscreenPlainSurface( UINT Width, UINT Height, D3DFORMAT Format, DWORD Pool, IDirect3DSurface9** ppSurface, HANDLE* pSharedHandle ); První dva parametry udávají šířku a výšku surface. Protože obrázek překryje celou obrazovku, nastavujeme mu rozměry SCREEN_WIDTH a SCREEN_HEIGHT. Třetím argumentem je barevný režim, který bude stejný jako u front a back bufferu a máme ho uložený v položce d3dFormat. Čtvrtým parametrem je tzv. pool, neboli typ paměti, kam má být surface uložen. Identifikátor D3DPOOL_DEFAULT říká, že bude umístěn do paměti, která se pro daný účel hodí. Nejčastěji je použita paměť na grafické kartě, což má výhodu v rychlejším přístupu. Nevýhodou naopak je to, že se data mohou ztratit, a v takovém případě se musí vše zrušit a zase vytvořit. Pátým parametrem je adresa, kam se uloží ukazatel na nově vzniklý surface. Poslední parametr se v současné době nepoužívá a má vždy hodnotu NULL. Jakmile je surface vytvořený, můžeme na něj uložit obrázek. Načtení ze souboru provádí funkce D3DXLoadSurfaceFromFile, o níž jsme si řekli, že je definovaná v hlavičkovém souboru d3dx9.h. Tato funkce má celkem osm parametrů:
4. Direct3D
HRESULT D3DXLoadSurfaceFromFile ( LPDIRECT3DSURFACE9 pDestSurface, CONST PALETTEENTRY* pDestPalette, CONST RECT* pDestRect, LPCTSTR pSrcFile, CONST RECT* pSrcRect, DWORD Filter, D3DCOLOR ColorKey, D3DXIMAGE_INFO* pSrcInfo ); První parametr je ukazatel na objekt rozhraní IDirect3DSurface9, do kterého má být obrázek načten. Druhým parametrem je ukazatel na strukturu typu PALETTEENTRY, kam se má ukládat paleta barev. Paleta barev se používá pro obrázky s 256 barvami, tedy takové, které mají 8bitovou barevnou hloubkou. My máme větší barevnou hloubku, proto v našem programu použijeme hodnotu NULL (žádná paleta barev není). Dále následuje ukazatel na strukturu typu RECT, tedy obdélníkovou oblast. To se používá v případě, že chceme nahrát bitmapu na konkrétní místo surface (rozměry bitmapy mohou být menší). Nám ale bitmapa zabere celý obsah surface a v takovém případě se tento parametr nastavuje na NULL. Čtvrtý parametr je textový řetězec, ve kterém je uložena cesta k souboru, ze kterého se má obrázek načíst. V našem případě toto udává makro FILEIMAGE. Přitom je podporováno jen několik poměrně hodně rozšířených formátů rastrových souborů – například *.bmp, *.png nebo *.jpg.
4.4 Surfaces v Direct3D
Prodáno 14.10.2014 na www.Kosmas.cz zákazníkovi [email protected], číslo objednávky 591522, UID: 7e15c933-e4b8-4e2a-94a6-c3fd2dffdec9
100
DIRECTX
Pátým parametrem je opět ukazatel na strukturu typu RECT. Tentokrát ovšem představuje obdélníkovou oblast zdrojového obrázku, která se má načíst. Jinými slovy, obrázek se nemusí načíst celý, ale jen jeho část, kterou touto strukturou definujeme. My však chceme načíst obrázek celý, a v takovém případě se tento parametr opět nastavuje na NULL. Následuje parametr DWORD Filter. Ten představuje sérii příznaků (filtrů), které se mají na přenášená data aplikovat. Tímto způsobem může docházet při přenosu k jisté modifikaci. Často se na místě tohoto argumentu používá symbolická konstanta D3DX_DEFAULT. Předposlední hodnota je tzv. barevný klíč. O tom, co to barevné klíče jsou a o práci s nimi pojednávala podkapitola 3.6. Stručně řečeno, můžeme zde nastavit jednu barvu, která bude brána jako průhledná, tedy přenášený obrázek nemusí být obdélníkový. Pokud chceme obdélníkový přenos a žádný barevný klíč není, dosazujeme zde hodnotu NULL. Poslední parametr je ukazatel na strukturu typu D3DXIMAGE_INFO, která obsahuje informace o obrázku (rozměry obrázku, barevná hloubka apod.). Tyto informace se doplní po zavolání funkce D3DXLoadSurfaceFromFile. Pokud tyto informace nepotřebujeme, můžeme zde také dosadit hodnotu NULL. Zbývá nám popsat poslední dvě metody. CWindow::UpdateFrame a CWindow::Restore. Zde je výpis první z nich: void CWindow::UpdateFrame(void) { LPDIRECT3DSURFACE9 lpBackBuffer; HRESULT d3dval; if((d3dval = m_lpD3DDevice->TestCooperativeLevel()) != D3D_OK) { if(d3dval == D3DERR_DEVICELOST) return; if(d3dval == D3DERR_DEVICENOTRESET) { Restore(); /* Terminate(); if(!InitializeDirect3D()) PostQuitMessage(0);*/ return; } } if((m_lpD3DDevice->GetBackBuffer(0, 0, D3DBACKBUFFER_TYPE_MONO, &lpBackBuffer)) != D3D_OK) { sprintf(g_tcError,TEXT("Nelze získat ukazatel na back buffer!")); PostQuitMessage(0); } if((m_lpD3DDevice->StretchRect(m_lpBackImage, NULL, lpBackBuffer, NULL, D3DTEXF_NONE)) != D3D_OK) { sprintf(g_tcError,TEXT("Nelze zkopirovat obrázek do back bufferu!")); PostQuitMessage(0); }
4. Direct3D
Prodáno 14.10.2014 na www.Kosmas.cz zákazníkovi [email protected], číslo objednávky 591522, UID: 7e15c933-e4b8-4e2a-94a6-c3fd2dffdec9
DIRECTX
101
m_lpD3DDevice->Present(NULL, NULL, NULL, NULL); RELEASE_DX_OBJECT(lpBackBuffer); } Tato metoda se cyklicky volá po dobu celého „života“ programu. Vlastně provádí stále stejnou činnost – přenáší obrázek do back bufferu a následně se obsah back bufferu přenese na obrazovku. Počáteční příkazy prozatím vynecháme a vrátíme se k nim později. Týkají se ztracení zobrazovacího zařízení. Pokud chceme přenést obrázek do back bufferu, nejdříve na něj potřebujeme získat ukazatel. To provádí metoda IDirect3DDevice9::GetBackBuffer. Má celkem čtyři parametry: HRESULT IDirect3DDevice9::GetBackBuffer( UINT iSwapChain, UINT BackBuffer, D3DBACKBUFFER_TYPE Type, IDirect3DSurface9 ** ppBackBuffer ); První parametr je identifikátor swapovacího řetězu, který nepoužíváme, proto je tu hodnota 0. Druhý argument je index back bufferu, jehož ukazatel chceme získat. Jak víme, back bufferů může být více a jsou číslovány od nuly. My ovšem máme jen jeden back buffer, je tedy i první a má index 0. Hodnota třetího parametru udává typ back bufferu, který je v DirectX9 vždy D3DBACKBUFFER_TYPE_MONO. Konečně poslední parametr představuje adresu, kam se má získaný ukazatel uložit (objekt rozhraní IDirect3DSurface9). Jakmile získáme ukazatel na back buffer, můžeme do něj přenést obrázek. Protože i back buffer je surface, jde vlastně o přenos bloku z jednoho surface do druhého. Tento přenos zajistíme zavoláním metody m_lpD3DDevice->StretchRect, která má následující podobu:
4. Direct3D
HRESULT IDirect3DDevice9::StretchRect( IDirect3DSurface9 * pSourceSurface, CONST RECT * pSourceRect, IDirect3DSurface9 * pDestSurface, CONST RECT * pDestRect, D3DTEXTUREFILTERTYPE Filter ); První parametr je ukazatel na zdrojový surface, odkud se budou data přenášet. S tím souvisí i druhý parametr, což je ukazatel na strukturu typu RECT udávající polohu a velikost přenášené oblasti. Pokud chceme přenést vše, používá se hodnota NULL. Třetí parametr je ukazatel na cílový surface (v našem případě tedy na back buffer) a čtvrtý je opět ukazatel na strukturu typu RECT, která udává místo na cílovém surface, kam se mají data přenést. Hodnota NULL říká, že se data budou přenášet na celý povrch. Posledním parametrem je filtr přenosu dat, který může být D3DTEXF_NONE, D3DTEXF_POINT nebo D3DTEXF_LINEAR. Jakmile jsou data do back bufferu přenesena, metodou Present se obsah back bufferu přenese do front bufferu, tedy i na obrazovku. Protože ukazatel na back buffer již nepotřebujeme, je posléze zrušen. Poslední věcí, kterou jsme dosud u metody UpdateFrame nepopsali, je práce se ztraceným zobrazovacím zařízením. O co jde? Ke ztrátě tohoto zařízení, případně surfaces může dojít v případech, kdy se z běžící aplikace přepneme do aplikace jiné nebo do prostředí Windows. Grafická data jsou totiž v drtivé většině případů uložena na grafické
4.4 Surfaces v Direct3D
Prodáno 14.10.2014 na www.Kosmas.cz zákazníkovi [email protected], číslo objednávky 591522, UID: 7e15c933-e4b8-4e2a-94a6-c3fd2dffdec9
102
DIRECTX
kartě a pokud se přepneme do jiné aplikace, o tato data přijdeme. Tuto ztrátu dat je nutné ošetřit, neboť při opětovném přepnutí do naší aplikace uvidíme jen černou obrazovku bez obrázku. Ztrátu dat můžeme zjistit zavoláním metody m_lpD3DDevice->TestCooperativeLevel(). Pokud je vše v pořádku (ke ztrátě dat nedošlo), tato metoda vrací hodnotu D3D_OK. Pokud se z aplikace přepneme někam jinam, v tom okamžiku již ke ztrátě zařízení dochází a metoda IDirect3DDevice9::TestCooperativeLevel začne vracet hodnotu D3DERR_DEVICELOST. V takovém případě v našem programu celá metoda UpdateFrame končí a nic se nepřenáší ani nevykresluje. Ve chvíli, kdy se ovšem přepneme do naší aplikace zpět, metoda IDirect3DDevice9::TestCooperativeLevel začne vracet hodnotu D3DERR_DEVICENOTRESET. To je právě chvíle, kdy je už jen na nás, abychom dali vše do pořádku. Ptáte se, co všechno musíme udělat? Existují dvě cesty. První z nich máme v našem výpisu v komentáři: /*
Terminate(); if(!InitializeDirect3D()) PostQuitMessage(0);
*/ Tímto vlastně celou aplikaci restartujeme. Nejdříve zrušíme všechny objekty a potom opět inicializujeme Direct3D. Ve skutečnosti ale nemusíme zajít tak daleko. Druhou možností (kterou používáme v našem programu a budeme ji používat i v dalších programech) je zavolání metody CWindow::Restore, což je poslední funkce, kterou jsme si dosud nepopsali: void CWindow::Restore() { D3DPRESENT_PARAMETERS d3dpp; D3DFORMAT d3dFormat; RELEASE_DX_OBJECT(m_lpBackImage); d3dFormat = (SCREEN_WIDTH == 32) ? D3DFMT_X8R8G8B8 : D3DFMT_R5G6B5; ZeroMemory(&d3dpp, sizeof(d3dpp)); d3dpp.Windowed = FALSE; d3dpp.SwapEffect = D3DSWAPEFFECT_DISCARD; d3dpp.BackBufferFormat = d3dFormat; d3dpp.BackBufferWidth = SCREEN_WIDTH; d3dpp.BackBufferHeight = SCREEN_HEIGHT; d3dpp.hDeviceWindow = m_hWnd; if((m_lpD3DDevice->Reset(&d3dpp)) != D3D_OK) { sprintf(g_tcError,TEXT("Nelze obnovit zařízení!")); PostQuitMessage(0); return; } Sleep(100);
4. Direct3D
Prodáno 14.10.2014 na www.Kosmas.cz zákazníkovi [email protected], číslo objednávky 591522, UID: 7e15c933-e4b8-4e2a-94a6-c3fd2dffdec9
DIRECTX
103
if((m_lpD3DDevice->CreateOffscreenPlainSurface(SCREEN_WIDTH, SCREEN_HEIGHT, d3dFormat, D3DPOOL_DEFAULT, &m_lpBackImage, NULL)) != D3D_OK) { sprintf(g_tcError,TEXT("Nelze vytvořit off-screen surface!")); PostQuitMessage(0); return; } if((D3DXLoadSurfaceFromFile(m_lpBackImage, NULL, NULL, FILEIMAGE, NULL, D3DX_DEFAULT, 0, NULL)) != D3D_OK) { sprintf(g_tcError,TEXT("Nelze načíst obrázek ze souboru!")); PostQuitMessage(0); return; } } Jak vidíme, nemusíme rušit všechno. Stačí zrušit surface, kam ukládáme obrázek, ale zařízení a objekt 3D zůstanou zachovány. U složitějších programů bychom takto museli zrušit všechny objekty jako textury, mesh objekty, vertex buffery apod., které jsou také uloženy v paměti grafické karty. Potom musíme zařízení obnovit. To provádí metoda m_lpD3DDevice->Reset. Tato metoda má jeden parametr, což je ukazatel na strukturu typu D3DPRESENT_PARAMETERS, kam jsme uložili stejné vlastnosti jako při inicializaci zařízení. Po úspěšném resetování počkáme desetinu vteřiny – na příkaz Sleep(100); – a poté musíme vše obnovit. V našem případě tedy opět vytvoříme off-screen surface a načteme do něj obrázek.
4. Direct3D
Program jsme si tedy popsali. Pokud zkompilujete a spustíte program dodaný s touto knihou, objeví se vám obrázek kamínků (soubor Image.bmp) – viz obrázek 4.5. Přestože popsaný program pracuje pouze s jedním načítaným obrázkem, neměl by nyní pro vás
Obrázek 4.5: Obrázek zobrazený pomocí Direct3D
4.4 Surfaces v Direct3D
Prodáno 14.10.2014 na www.Kosmas.cz zákazníkovi [email protected], číslo objednávky 591522, UID: 7e15c933-e4b8-4e2a-94a6-c3fd2dffdec9
104
DIRECTX
být problém připravit potřebné úpravy, aby program načítal obrázků více a pracoval s jejich částmi. Tímto způsobem se dají vytvořit i mnohem složitější aplikace.
4.5 Vykreslování jednoduchých objektů Nyní nastává ten správný čas, kdy si ukážeme vykreslení jednoduchých 3D objektů. Půjde o body, úsečky a trojúhelníky. Možná vás napadne, že kreslení takových geometrických objektů těžko prakticky využijete. Ve skutečnosti se ale jakýkoliv složitý objekt typu mesh (tedy objekt je popsán vertexy, hranami a plochami) skládá z trojúhelníkových ploch. Pro názornost se můžete zpětně podívat na model stolu na obrázku 4.4. V našem programu si prozatím ukážeme pouze kreslení těchto jednoduchých grafických prvků, které se budou vykreslovat definovanou barvou na bílé pozadí. Pokud jste ale prostudovali předchozí kapitolu, neměl by být pro vás problém program upravit tak, aby byl v pozadí nějaký obrázek. Pro kreslení bodů, úseček a trojúhelníků potřebujeme nutně vertex buffer. Jak název napovídá, vertex buffer uchovává vrcholy (vertexy), které se budou vykreslovat samostatně (body), nebo se spojují dvojice či trojice těchto bodů (takto vzniknou úsečky a trojúhelníky). Z uvedeného popisu vyplývá, že vykreslování těchto objektů by nemělo být složité naprogramovat. Následující odstavce prozradí, jak na to. Budeme mít opět známou šestici souborů a vyjdeme znovu z programu Direct3D, popsaného v podkapitole 4.2. V souboru Global.h přibylo pět nových symbolických konstant: #define SCREEN_WIDTH #define SCREEN_HEIGHT #define SCREEN_DEPTH
640 480 32
#define NUMBER_OF_VERTICES 6 #define D3DFVF_CUSTOMVERTEX (D3DFVF_XYZRHW|D3DFVF_DIFFUSE) První tři makra jsou nám známá. Aplikace bude opět přes celou obrazovku a SCREEN_ WIDTH, SCREEN_HEIGHT a SCREEN_DEPTH udávají rozlišení obrazovky a barevnou hloubku. Symbolická konstanta NUMBER_OF_VERTICES definuje počet vertexů (pro jednoduchost jich budeme mít jen šest). Poslední makro D3DFVF_CUSTOMVERTEX udává formát vertexů. Je to kombinace dvou příznaků, kterou v naši aplikaci použijeme vícekrát. To je důvod, proč jsme toto makro vytvořili. Příznak D3DFVF_XYZRHW říká, že souřadnice bodů umístěných ve vertex bufferu budou odpovídat souřadnicím pixelů na obrazovce. Naproti tomu příznak D3DFVF_DIFFUSE znamená, že se používají i barevné hodnoty přiřazené pixelům. Do souborů WinMain.cpp, CApplication.h a CApplication.cpp nemusíme vůbec zasahovat. Všechny další změny se budou týkat souborů CWindow.h a CWindow.cpp. Do souboru CWindow.h ještě před třídu CWindow umístíme deklaraci struktury: struct CUSTOMVERTEX { float x, y, z, w; DWORD color; }; Tato struktura představuje šablonu pro jednotlivé vertexy. Jak víme, každý vertex je dán svými souřadnicemi x, y, z a w představuje váhu bodu. Hodnota color bude udávat barvu.
4. Direct3D
Prodáno 14.10.2014 na www.Kosmas.cz zákazníkovi [email protected], číslo objednávky 591522, UID: 7e15c933-e4b8-4e2a-94a6-c3fd2dffdec9
DIRECTX
105
Určitými změnami prošla i třída CWindow: class CWindow { private: HWND m_hWnd; LPDIRECT3D9 m_lpD3D; LPDIRECT3DDEVICE9 m_lpD3DDevice; LPDIRECT3DVERTEXBUFFER9 m_lpD3DVertexBuffer; D3DPRESENT_PARAMETERS m_D3Dpp; D3DFORMAT m_D3DFormat; CUSTOMVERTEX m_aVertices[NUMBER_OF_VERTICES]; CUSTOMVERTEX *m_pVertices; public: CWindow(void); ~CWindow(void); bool Initialize(void); void Terminate(void); HWND GetHWnd(void) const{return (m_hWnd);} bool CreateSources(void);
friend LRESULT CALLBACK WndProc(HWND m_hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam); }; Přibylo nám zde pět atributů. Jako první je to m_lpD3DVertexBuffer, což je ukazatel na rozhraní vertex bufferu, který budeme muset vytvořit (IDirect3DVertexBuffer9). Potom zde máme dva známé atributy D3DPRESENT_PARAMETERS m_D3Dpp a D3DFORMAT m_D3DFormat, které jsme přesunuli z lokálních proměnných do členských atributů. Důvodem přesunu bylo, že je v metodách používáme vícekrát, takže se tímto způsobem vyhneme jejich opětovnému vytváření a naplňování. Nově zde máme i pole vertexů CUSTOMVERTEX m_aVertices[NUMBER_OF_VERTICES], kam uložíme a budeme uchovávat souřadnice a barvy všech šesti vertexů. Posledním atributem je CUSTOMVERTEX *m_pVertices, který představuje ukazatel na místo, kam se přesunou hodnoty vertexů pro vertex buffer (viz dále).
4. Direct3D
void UpdateFrame(void);
K metodám třídy CWindow přibyla nová metoda CreateSources. Jejím smyslem je vytvoření vertex bufferu a jeho naplnění. Možná vás napadne otázka, proč jsme toto nevložili do metody CWindow::Initialize. Důvod je jednoduchý – tato činnost se provádí nejen při inicializaci, ale i v případě, kdy vertex buffer ztratíme. Tím, že jsme si tuto metodu vytvořili, nemusíme psát stejný kód dvakrát, v případě potřeby stačí tuto metodu pouze zavolat.
4.5 Vykreslování jednoduchých objektů
Prodáno 14.10.2014 na www.Kosmas.cz zákazníkovi [email protected], číslo objednávky 591522, UID: 7e15c933-e4b8-4e2a-94a6-c3fd2dffdec9
106
DIRECTX
Jako obvykle nalezneme v souboru CWindow.cpp definovaná těla metod třídy CWindow. Díky novým atributům je počáteční inicializace a tím pádem i tělo konstruktoru delší: CWindow::CWindow(void) { m_hWnd = NULL; m_lpD3D = NULL; m_lpD3DDevice = NULL; m_lpD3DVertexBuffer = NULL; ZeroMemory(&m_D3Dpp, sizeof(m_D3Dpp)); m_D3DFormat = (SCREEN_WIDTH == 32) ? D3DFMT_X8R8G8B8 : D3DFMT_R5G6B5; m_aVertices[0].x = 300.0f; m_aVertices[0].y = 200.0f; m_aVertices[0].z = 0.0f; m_aVertices[0].w = 1.0f; m_aVertices[0].color = D3DCOLOR_XRGB(0, 0, 0); m_aVertices[1].x = 350.0f; m_aVertices[1].y = 200.0f; m_aVertices[1].z = 0.0f; m_aVertices[1].w = 1.0f; m_aVertices[1].color = D3DCOLOR_XRGB(0, 0, 0); m_aVertices[2].x = 250.0f; m_aVertices[2].y = 250.0f; m_aVertices[2].z = 0.0f; m_aVertices[2].w = 1.0f; m_aVertices[2].color = D3DCOLOR_XRGB(0, 0, 0); m_aVertices[3].x = 400.0f; m_aVertices[3].y = 250.0f; m_aVertices[3].z = 0.0f; m_aVertices[3].w = 1.0f; m_aVertices[3].color = D3DCOLOR_XRGB(0, 0, 0); m_aVertices[4].x = 300.0f; m_aVertices[4].y = 300.0f; m_aVertices[4].z = 0.0f; m_aVertices[4].w = 1.0f; m_aVertices[4].color = D3DCOLOR_XRGB(0, 0, 0); m_aVertices[5].x = 350.0f; m_aVertices[5].y = 300.0f; m_aVertices[5].z = 0.0f; m_aVertices[5].w = 1.0f; m_aVertices[5].color = D3DCOLOR_XRGB(0, 0, 0); } Pokud přeskočíme známé příkazy na začátku konstruktoru, zbytek těla této metody tvoří naplnění pole šesti vertexy. Již na začátku jsme si řekli, že hodnoty x, y a z budou před-
4. Direct3D
Prodáno 14.10.2014 na www.Kosmas.cz zákazníkovi [email protected], číslo objednávky 591522, UID: 7e15c933-e4b8-4e2a-94a6-c3fd2dffdec9
DIRECTX
107
stavovat přímo souřadnice na obrazovce, proto jsou souřadnice z vždy nulové a váha bodu w jako obvykle na hodnotě 1. Jenom připomeňme, že počátkem souřadnicového systému obrazovky je levý horní roh. Rozmístění bodů podle zadaných souřadnic vypadá tak, jak ukazuje obrázek 4.6 vlevo. Pochopitelně si můžete souřadnice upravit a rozmístění bodu tak změnit. Zvolená vzájemná poloha bodů ve výše uvedeném výpisu je jen kvůli názornějšímu popisu. Barvu bodů máme vždy nastavenou černou (pozadí bude bílé). Opět ale můžete experimentovat a barvy podle potřeby změnit – každý vertex má svoji barvu, a tak na obrazovce můžete u úseček a hlavně trojúhelníků dosáhnout zajímavých barevných kombinací a přechodů.
Obrázek 4.6: Vykreslení objektů – body (vlevo) a úsečky (vpravo)
Tělo destruktoru ruší všechny objekty a uvolňuje ukazatele. Nově zde přibylo i rušení vertex bufferu:
CloseWindow(m_hWnd); }
4. Direct3D
void CWindow::Terminate(void) { RELEASE_DX_OBJECT(m_lpD3DVertexBuffer); RELEASE_DX_OBJECT(m_lpD3DDevice); RELEASE_DX_OBJECT(m_lpD3D);
Tělo metody CWindow::Initialize obsahuje většinu známého kódu, který jsme si popsali v předchozích kapitolách. Vytváří se zde okno aplikace, provádí se inicializace Direct3D objektu a zobrazovacího zařízení: bool CWindow::Initialize(void) { WNDCLASS wc;
ZeroMemory(&wc, sizeof(wc)); wc.style = CS_HREDRAW|CS_VREDRAW; wc.lpfnWndProc = WndProc; wc.hInstance = GetModuleHandle(NULL); wc.hIcon = LoadIcon(NULL, IDI_WINLOGO); wc.hCursor = LoadCursor(NULL, IDC_ARROW); wc.hbrBackground = (HBRUSH) GetStockObject(WHITE_BRUSH); wc.lpszClassName = APP_NAME;
4.5 Vykreslování jednoduchých objektů
Prodáno 14.10.2014 na www.Kosmas.cz zákazníkovi [email protected], číslo objednávky 591522, UID: 7e15c933-e4b8-4e2a-94a6-c3fd2dffdec9
108
DIRECTX
if(!RegisterClass(&wc)) { sprintf(g_tcError,TEXT("Chyba při registraci okna aplikace!")); return (false); } if(!(m_hWnd = CreateWindowEx(0, APP_NAME, APP_NAME, WS_OVERLAPPEDWINDOW, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, NULL, NULL, wc.hInstance, NULL))) { sprintf(g_tcError,TEXT("Chyba při vytváření okna aplikace!")); return (false); } if(!(m_lpD3D = Direct3DCreate9(D3D_SDK_VERSION))) { sprintf(g_tcError,TEXT("Nelze vytvořit objekt Direct3D!")); return (false); } m_D3Dpp.Windowed = false; m_D3Dpp.SwapEffect = D3DSWAPEFFECT_DISCARD; m_D3Dpp.BackBufferFormat = m_D3DFormat; m_D3Dpp.BackBufferWidth = SCREEN_WIDTH; m_D3Dpp.BackBufferHeight = SCREEN_HEIGHT; m_D3Dpp.hDeviceWindow = m_hWnd; if((m_lpD3D->CreateDevice(0, D3DDEVTYPE_HAL, m_hWnd, D3DCREATE_SOFTWARE_VERTEXPROCESSING, &m_D3Dpp, &m_lpD3DDevice)) != D3D_OK) { sprintf(g_tcError,TEXT("Nelze vytvořit objekt Direct3DDevice!")); return (false); } if(!CreateSources()) return (false); return (true); } Na konci si všimněme volání metody CWindow::CreateSources, která – jak jsme uvedli – vytváří vertex buffer a naplňuje ho údaji: bool CWindow::CreateSources(void) { if((m_lpD3DDevice->CreateVertexBuffer((sizeof(CUSTOMVERTEX) * NUMBER_OF_VERTICES), 0, D3DFVF_CUSTOMVERTEX, D3DPOOL_DEFAULT, &m_lpD3DVertexBuffer, NULL)) != D3D_OK)
4. Direct3D
Prodáno 14.10.2014 na www.Kosmas.cz zákazníkovi [email protected], číslo objednávky 591522, UID: 7e15c933-e4b8-4e2a-94a6-c3fd2dffdec9
DIRECTX
109
{ sprintf(g_tcError,TEXT("Nelze vytvořit vertex buffer!")); return (false); } if((m_lpD3DVertexBuffer->Lock(0, 0, (void**) &m_pVertices, 0)) != D3D_OK) { sprintf(g_tcError,TEXT("Nelze zamknout vertex buffer!")); return (false); } memcpy(m_pVertices, m_aVertices, sizeof(m_aVertices)); if((m_lpD3DVertexBuffer->Unlock())!=D3D_OK) { sprintf(g_tcError,TEXT("Nelze odemknout vertex buffer!")); return (false); } return (true); Vytvoření vertex bufferu provádí metoda IDirect3DDevice9::CreateVertexBuffer, která má celkem šest parametrů. První parametr, který požaduje, je celková velikost bufferu. Druhý parametr udává způsob použití bufferu. Zde máme hodnotu 0, což značí, že nechceme způsob použití definovat. K dispozici je asi třináct příznaků, jimiž můžeme specifikovat vlastnosti, například D3DUSAGE_DYNAMIC informující o tom, že se mohou měnit data uložená ve vertex bufferu, nebo D3DUSAGE_WRITEONLY říkající, že do bufferu můžeme data jen zapsat, ale nedají se z něj přečíst. Třetí argument udává formát vrcholu, který máme definován symbolickou konstantou D3DFVF_CUSTOMVERTEX. Potom následuje typ použité paměti, kde se vertex buffer vytvoří. S tímto jsme se již setkali, protože tento argument má stejný význam jako čtvrtý parametr funkce CreateOffscreenPlainSurface. D3DPOOL_DEFAULT říká, že se pro uložení dat vybere nejvhodnější paměť, zpravidla paměť na grafické kartě. Předposledním parametrem je adresa ukazatele pro objekt rozhraní vytvořeného vertex bufferu. Poslední parametr se nepoužívá a jeho hodnota je vždy NULL.
4. Direct3D
}
Pokud se nám podaří vertex buffer vytvořit, je třeba ho naplnit daty, tedy vertexy. To ale můžeme provádět jen tehdy, pokud je vertex buffer zamčený, protože při zápisu do něj potřebujeme výhradní přístup a je nežádoucí, aby se s ním mohlo během plnění daty cokoliv dít. Zamčení vertex bufferu provádí metoda IDirect3DVertexBuffer9::Lock. Tato metoda vyžaduje celkem čtyři parametry. První z nich je index zamčeného bytu ve vertex bufferu, druhý počet zamčených bytů. Pokud chceme zamknout všechny vertexy (náš případ), oba parametry se nastavují na hodnotu 0. Třetím argumentem je adresa, kam se má uložit ukazatel na data náležející vertex bufferu, což je v našem případě *m_pVertices. Posledním, čtvrtým parametrem je kombinace příznaků, jež určují způsob zamknutí bufferu. My žádné konkrétní nastavení uzamčení nepotřebujeme, proto je v našem programu nastavena hodnota 0. Poté, co vertex buffer zamkneme, můžeme do něj zapsat data. K tomu používáme funkci jazyka C memcpy. První argument této funkce označuje začátek cílové oblasti, druhý je začátek zdrojové oblasti a poslední je množství přenášených dat. Po přenosu vertex buffer
4.5 Vykreslování jednoduchých objektů
Prodáno 14.10.2014 na www.Kosmas.cz zákazníkovi [email protected], číslo objednávky 591522, UID: 7e15c933-e4b8-4e2a-94a6-c3fd2dffdec9
110
DIRECTX
odemkneme (to provádí metoda IDirect3DVertexBuffer9::Unlock) a celá metoda CWindow::CreateSources se ukončí. Zbývá popsat metodu CWindow::UpdateFrame. Tato metoda se volá stále cyklicky až do ukončení programu s frekvencí danou makrem WAITING_FOR_FLIP a provádí několik specifikovaných činností. První z nich je blok, v němž dochází k testování, zda nedošlo ke ztrátě zařízení: HRESULT d3dval; if((d3dval = m_lpD3DDevice->TestCooperativeLevel()) != D3D_OK) { if(d3dval == D3DERR_DEVICELOST) return; if(d3dval == D3DERR_DEVICENOTRESET) { RELEASE_DX_OBJECT(m_lpD3DVertexBuffer); if((m_lpD3DDevice->Reset(&m_D3Dpp)) != D3D_OK) { sprintf(g_tcError,TEXT("Nelze obnovit zařízení!")); PostQuitMessage(0); return; } Sleep(100); if(!CreateSources()) PostQuitMessage(0); return; } } Postup testování jsme si vysvětlili v předchozí podkapitole. Rozdílná je pouze reakce. V předchozí části jsme obnovovali off-screen surface, tady provádíme totéž, ale pro vertex buffer. Ten nejdříve rušíme příkazem RELEASE_DX_OBJECT(m_lpD3DVertexBuffer), potom obnovíme zařízení (metoda m_lpD3DDevice->Reset(&m_D3Dpp)) a následně vertex buffer opětovně vytvoříme tak, že zavoláme metodu CWindow::CreateSources. Následně již můžeme začít vykreslovat: m_lpD3DDevice->Clear(0, NULL, D3DCLEAR_TARGET, D3DCOLOR_XRGB(255, 255, 255), 0, 0); if((m_lpD3DDevice->BeginScene()) != D3D_OK) { sprintf(g_tcError,TEXT("Nelze začít vykreslovat scénu!")); PostQuitMessage(0); } if((m_lpD3DDevice->SetStreamSource(0, m_lpD3DVertexBuffer, 0, sizeof(CUSTOMVERTEX))) != D3D_OK) {
4. Direct3D
Prodáno 14.10.2014 na www.Kosmas.cz zákazníkovi [email protected], číslo objednávky 591522, UID: 7e15c933-e4b8-4e2a-94a6-c3fd2dffdec9
DIRECTX
111
sprintf(g_tcError,TEXT("Nelze nastavit vstupní vertex buffer!")); PostQuitMessage(0); } if((m_lpD3DDevice->SetFVF(D3DFVF_CUSTOMVERTEX)) != D3D_OK) { sprintf(g_tcError,TEXT("Nelze nastavit stínování objektů!")); PostQuitMessage(0); } if((m_lpD3DDevice->DrawPrimitive(D3DPT_POINTLIST, 0, 6)) != D3D_OK) { sprintf(g_tcError,TEXT("Nelze vykreslit objekty!")); PostQuitMessage(0); } if((m_lpD3DDevice->EndScene()) != D3D_OK) { sprintf(g_tcError,TEXT("Nelze ukončit vykreslování scény!")); PostQuitMessage(0); }
Nejprve vymažeme metodou IDirect3DDevice9::Clear obrazovku (vyplníme ji bílou barvou). Potom zavoláme metodu IDirect3DDevice9::BeginScene, která udává začátek vykreslování. Po vykreslení všech objektů je třeba na konci zavolat ukončovací metodu IDirect3DDevice9::EndScene. Mezitím, ještě než se bude vykreslovat, se volá metoda IDirect3DDevice9::SetStreamSource, kterou nastavujeme umístění vertexů, tedy vstupní vertex buffer, z něhož se budou vertexy brát. Prvním parametrem této metody je použitý datový tok. Hodnota tohoto argumentu je většinou 0. Dále následuje ukazatel na vertex buffer, ze kterého se budou data vykreslovat. Třetí parametr specifikuje tzv. offset, tedy počet bytů od začátku vertex bufferu do začátku vertexů ve vertex bufferu. Protože máme vertexy uloženy hned od začátku bufferu, je hodnota tohoto argumentu 0. Tímto způsobem můžeme vynechat první vertexy, tedy zajistit, aby se nevykreslily. Posledním parametrem metody IDirect3DDevice9::SetStreamSource je velikost každé sady vertexů.
4. Direct3D
m_lpD3DDevice->Present(NULL, NULL, NULL, NULL);
Obrázek 4.7: Vykreslení objektů – navazující úsečky (vlevo) a trojúhelníky (vpravo)
4.5 Vykreslování jednoduchých objektů
Prodáno 14.10.2014 na www.Kosmas.cz zákazníkovi [email protected], číslo objednávky 591522, UID: 7e15c933-e4b8-4e2a-94a6-c3fd2dffdec9
112
DIRECTX
Obrázek 4.8: Vykreslení objektů – pruh trojúhelníků (vlevo) a vějíř trojúhelníků (vpravo)
Posledním příkazem před vykreslením je IDirect3DDevice9::SetFVF. Tato metoda nastavuje stínování vertexů. Jediným parametrem této funkce je kombinace příznaků formátu vrcholu. Tyto příznaky jsou uloženy v symbolické konstantě D3DFVF_CUSTOMVERTEX. Samotné vykreslení objektů provede metoda IDirect3DDevice9::DrawPrimitive. Tato metoda má celkem tři argumenty. První parametr udává, co se má vykreslovat. K dispozici jsou následující možnosti:
D3DPT_POINTLIST – kreslení vertexů,
D3DPT_LINELIST – kreslení úseček,
D3DPT_LINESTRIP – kreslení navazujících úseček,
D3DPT_TRIANGLELIST – kreslení trojúhelníků,
D3DPT_TRIANGLESTRIP – kreslení pruhu trojúhelníků,
D3DPT_TRIANGLEFAN – kreslení vějířů trojúhelníků.
Druhým parametrem je vrchol, ve kterém se začíná kreslit. Třetím parametrem je počet objektů, jež mají být vykresleny. Pro vykreslení bodů (výsledek je na obrázku 4.6 vlevo) vypadá volání této metody v našem programu následovně: m_lpD3DDevice->DrawPrimitive(D3DPT_POINTLIST, 0, 6); Na obrázku 4.6 v pravé části jsou vykresleny úsečky. Zde by tedy vypadalo volání metody IDirect3DDevice9::DrawPrimitive takto: m_lpD3DDevice->DrawPrimitive(D3DPT_LINELIST, 0, 3); Obdobně bude vypadat kreslení navazujících úseček (obrázek 4.7 vlevo): m_lpD3DDevice->DrawPrimitive(D3DPT_LINESTRIP, 0, 5); U trojúhelníků nastává malý problém. Vykreslované trojúhelníky se vyplňují barvou, která je ovlivňována všemi třemi vrcholy (čím blíže je vykreslovaný bod vrcholu, tím více ho barva ovlivňuje). Problém s vykreslením ovšem spočívá v tom, že polohy indexů bodů ve vertex bufferu musí být umístěny ve směru hodinových ručiček. V opačném případě totiž trojúhelník nebude vidět, protože se bude trojúhelník vyplňovat z odvrácené strany. Vykreslování jednouchých trojúhelníků (obrázek 4.7 vpravo) probíhá po trojicích vertexů. Máme tedy celkem dva trojúhelníky (indexy vertexů 1, 2, 3 a 4, 5, 6). Příkaz vypadá takto:
4. Direct3D
Prodáno 14.10.2014 na www.Kosmas.cz zákazníkovi [email protected], číslo objednávky 591522, UID: 7e15c933-e4b8-4e2a-94a6-c3fd2dffdec9
DIRECTX
113
m_lpD3DDevice->DrawPrimitive(D3DPT_TRIANGLELIST, 0, 2); Pro pruh trojúhelníků (obrázek 4.8 vlevo) je příznačné, že následující trojúhelník vznikne tak, že se vezmou (poslední) dva body předchozího trojúhelníku a přidá se další bod v pořadí, uložený ve vertex bufferu. V našem příkladě tak budeme mít čtyři trojúhelníky: indexy vertexů 1, 2, 3; 2, 3, 4; 3, 4, 5 a 4, 5, 6. Příkaz pro jejich vykreslení vypadá takto: m_lpD3DDevice->DrawPrimitive(D3DPT_TRIANGLESTRIP, 0, 4); Pokud budeme chtít kreslit vějíře trojúhelníků, i pro šestici vrcholů získáme čtveřici trojúhelníků (obrázek 4.8 vpravo). Zde ovšem platí, že všechny trojúhelníky mají jeden společný vrchol (střed vějíře). Trojúhelníky se budou vytvářet následovně: 1, 2, 3; 1, 3, 4; 1, 4, 5 a 1, 5, 6. Volání metody IDirect3DDevice9::DrawPrimitive by nyní vypadalo takto: m_lpD3DDevice->DrawPrimitive(D3DPT_TRIANGLEFAN, 0, 4);
4.6 Práce s index bufferem
4. Direct3D
Vertex buffery obsahují posloupnosti vertexů. Tyto posloupnosti přitom musí být uspořádány podle konečné potřeby vzhledu objektů. I pro málo složitější objekty než trojúhelníky je však takové uchovávání nevhodné, protože každý vertex musíme uchovávat tolikrát, kolik se v něm sbíhá hran či kolik ploch v tomto bodě hraničí. Vezměme si například krychli, která je na obrázku 4.9. Pokud bychom chtěli tuto krychli vykreslovat jen úsečkami (hranami) s použitím vertex bufferu tak, jak jsme si uvedli v předchozí části, potřebovali bychom uchovávat 24 vertexů. Krychle jich má sice jen osm, ale každý bychom museli uchovávat třikrát – pro každou hranu, která z vertexu vychází.
Obrázek 4.9: Krychle s očíslovanými vertexy
Takové uchovávání je paměťově neefektivní, proto se používají index buffery. Zatímco do vertex bufferu uložíme každý z vertexů jednou, index buffer obsahuje indexy jednotlivých vertexů. A na základě uspořádaných indexů se potom objekty vykreslují. Tento způsob vykreslování je paměťově mnohem efektivnější. Pokud se vrátíme k našemu příkladu krychle, budeme uchovávat osm vertexů a dvacet čtyři indexů. Právě krychli uvedenou na obrázku 4.9 si jako příklad použití index bufferů vykreslíme na obrazovku. Přitom se stále budeme pohybovat v rovině, tedy z-ové souřadnice vertexů zůstanou na nulové hodnotě (do prostoru se podíváme až později). Vertexy přitom uspořádáme tak, jak nám napovídají číslice na obrázku – čísla budou odpovídat indexům v poli vertexů. Příklad je podobný tomu z předchozí podkapitoly, proto si ponecháme stejné soubory. Pouze si popíšeme úpravy, které jsme provedli. V souboru Global.h jsme změnili hodno-
4.6 Práce s index bufferem
Prodáno 14.10.2014 na www.Kosmas.cz zákazníkovi [email protected]
114
DIRECTX
tu v makru NUMBER_OF_VERTICES (počet vertexů) a přidali nové makro NUMBER_OF_INDEXES (počet indexů pro index buffer): #define NUMBER_OF_VERTICES 8 #define NUMBER_OF_INDEXES 24 Soubory WinMain.cpp, CApplication.h a CApplication.cpp zůstaly nezměněny. V souboru CWindow.cpp prošla třída CWindow několika změnami: class CWindow { private: HWND
m_hWnd;
LPDIRECT3D9 m_lpD3D; LPDIRECT3DDEVICE9 m_lpD3DDevice; LPDIRECT3DVERTEXBUFFER9 m_lpD3DVertexBuffer; LPDIRECT3DINDEXBUFFER9 m_lpD3DIndexBuffer; D3DPRESENT_PARAMETERS m_D3Dpp; D3DFORMAT m_D3DFormat; CUSTOMVERTEX m_aVertices[NUMBER_OF_VERTICES]; WORD m_aIndices[NUMBER_OF_INDEXES*2]; CUSTOMVERTEX *m_pVertices; WORD *m_pIndex; public: CWindow(void); ~CWindow(void); bool Initialize(void); void Terminate(void); HWND GetHWnd(void) const{return (m_hWnd);} bool CreateSources(void); void UpdateFrame(void); friend LRESULT CALLBACK WndProc(HWND m_hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam); }; Metody zůstaly stejné, ale přibyly celkem tři atributy. První je m_lpD3DIndexBuffer, což je ukazatel rozhraní index bufferu (IDirect3DIndexBuffer9), který budeme používat. Potom je to pole indexů m_aIndices. Indexy jsou ukládány jako WORD, proto je potřeba pro každý z nich rezervovat 2 byty. Jako poslední je ukazatel *m_pIndex, který bude sloužit pro uložení indexů náležících index bufferu (je to obdoba ukazatele *m_pVertices pro vertex buffer). Nyní se přesuneme do souboru Window.cpp. V konstruktoru třídy CWindow přibyla inicializace ukazatele na index buffer:
4. Direct3D
Prodáno 14.10.2014 na www.Kosmas.cz zákazníkovi [email protected], číslo objednávky 591522, UID: 7e15c933-e4b8-4e2a-94a6-c3fd2dffdec9
DIRECTX
115
m_lpD3DIndexBuffer = NULL; Také se změnila inicializace vrcholů pro vertex buffer. Jak jsme si uvedli, indexy jsou uspořádány stejně, jak je uvedeno na obrázku 4.9, tedy x-ové a y-ové souřadnice jednotlivých bodů byly navrženy tak, aby se krychle vykreslovala uprostřed obrazovky. Barva byla u všech bodů ponechána černá, ale pokud chcete, stejně jako v předchozím příkladu s ní můžete experimentovat: m_aVertices[0].x = 280.0f; m_aVertices[0].y = 280.0f; m_aVertices[0].z = 0.0f; m_aVertices[0].w = 1.0f; m_aVertices[0].color = D3DCOLOR_XRGB(0, 0, 0); m_aVertices[1].x = 360.0f; m_aVertices[1].y = 280.0f; m_aVertices[1].z = 0.0f; m_aVertices[1].w = 1.0f; m_aVertices[1].color = D3DCOLOR_XRGB(0, 0, 0); m_aVertices[2].x = 360.0f; m_aVertices[2].y = 200.0f; m_aVertices[2].z = 0.0f; m_aVertices[2].w = 1.0f; m_aVertices[2].color = D3DCOLOR_XRGB(0, 0, 0);
m_aVertices[4].x = 320.0f; m_aVertices[4].y = 260.0f; m_aVertices[4].z = 0.0f; m_aVertices[4].w = 1.0f; m_aVertices[4].color = D3DCOLOR_XRGB(0, 0, 0);
4. Direct3D
m_aVertices[3].x = 280.0f; m_aVertices[3].y = 200.0f; m_aVertices[3].z = 0.0f; m_aVertices[3].w = 1.0f; m_aVertices[3].color = D3DCOLOR_XRGB(0, 0, 0);
m_aVertices[5].x = 400.0f; m_aVertices[5].y = 260.0f; m_aVertices[5].z = 0.0f; m_aVertices[5].w = 1.0f; m_aVertices[5].color = D3DCOLOR_XRGB(0, 0, 0); m_aVertices[6].x = 400.0f; m_aVertices[6].y = 180.0f; m_aVertices[6].z = 0.0f; m_aVertices[6].w = 1.0f; m_aVertices[6].color = D3DCOLOR_XRGB(0, 0, 0); m_aVertices[7].x = 320.0f; m_aVertices[7].y = 180.0f; m_aVertices[7].z = 0.0f;
4.6 Práce s index bufferem
Prodáno 14.10.2014 na www.Kosmas.cz zákazníkovi [email protected], číslo objednávky 591522, UID: 7e15c933-e4b8-4e2a-94a6-c3fd2dffdec9
116
DIRECTX
m_aVertices[7].w = 1.0f; m_aVertices[7].color = D3DCOLOR_XRGB(0, 0, 0); Dále v konstruktoru naplníme indexy pro index buffer. Naplnění vertexů i indexů by se mohlo načítat ze souboru, což by bylo při jejich větším počtu nejspíše nevyhnutelné. Pro jednoduchost si je nyní naplňme přímo. Připomeňme, že indexy jsou uloženy ve dvou bytech, přičemž ten vyšší se ukládá jako první. Proto jsou všechny sudé indexy vynulovány (na začátku jsou příkazem ZeroMemory vynulovány všechny, a tam, kam potřebujeme uložit hodnoty, je ukládáme). Z pořadí indexů je patrné, jak se krychle bude vykreslovat. Vykreslují se pouze úsečky, tedy hrany. Například čelní strana je dána indexy vertexů 0-1, 1-2, 2-3 a 3-0. ZeroMemory(&m_aIndices, sizeof(m_aIndices)); m_aIndices[0] = 0; m_aIndices[2] = 1; m_aIndices[4] = 1; m_aIndices[6] = 2; m_aIndices[8] = 2; m_aIndices[10] = 3; m_aIndices[12] = 3; m_aIndices[14] = 0; m_aIndices[16] m_aIndices[18] m_aIndices[20] m_aIndices[22] m_aIndices[24] m_aIndices[26] m_aIndices[28] m_aIndices[30]
= = = = = = = =
4; 5; 5; 6; 6; 7; 7; 4;
m_aIndices[32] m_aIndices[34] m_aIndices[36] m_aIndices[38] m_aIndices[40] m_aIndices[42] m_aIndices[44] m_aIndices[46]
= = = = = = = =
0; 4; 1; 5; 2; 6; 3; 7;
V destruktoru, respektive metodě CWindow::Terminate, která se z destruktoru volá, se navíc provádí zrušení objektu vertex bufferu a uvolnění ukazatele na něj makrem: RELEASE_DX_OBJECT(m_lpD3DIndexBuffer); Metoda CWindow::Initialize se nezměnila nijak, ale k několika změnám došlo v metodě CWindow::CreateSources, která – jak víme – je také součástí inicializace. Zde přibyla část, kde se vytváří index buffer a naplňuje jej indexy. Vše je velice podobné vytváření a naplňování vertex bufferu, což se v této metodě provádí také. Zde se ale nic nezměnilo, proto si uveďme jen část s index bufferem: if((m_lpD3DDevice->CreateIndexBuffer(sizeof(m_aIndices), 0, D3DFMT_INDEX32, D3DPOOL_DEFAULT, &m_lpD3DIndexBuffer, NULL)) != D3D_OK)
4. Direct3D
Prodáno 14.10.2014 na www.Kosmas.cz zákazníkovi [email protected], číslo objednávky 591522, UID: 7e15c933-e4b8-4e2a-94a6-c3fd2dffdec9
DIRECTX
117
{ sprintf(g_tcError,TEXT("Nelze vytvořit index buffer!")); return (false); } if((m_lpD3DIndexBuffer->Lock(0, 0, (void**)&m_pIndex, 0)) != D3D_OK) { sprintf(g_tcError,TEXT("Nelze zamknout index buffer!")); return (false); } memcpy(m_pIndex, m_aIndices, sizeof(m_aIndices)); if((m_lpD3DIndexBuffer->Unlock()) != D3D_OK) { sprintf(g_tcError,TEXT("Nelze odemknout index buffer!")); return (false); }
Poté následuje vyplňování obsahu index bufferu hodnotami z pole m_aIndices. K tomu je třeba nejdříve index buffer zamknout (metoda IDirect3DIndexBuffer9::Lock) a po přenesení hodnot se funkcí memcpy opět odemkne (metoda IDirect3DIndexBuffer9:: Unlock). Parametry metody Lock jsou identické jako u stejnojmenné metody rozhraní IDirect3DVertexBuffer9. Poslední změny v programu se týkají metody CWindow::UpdateFrame. První drobná změna je v první části této metody, kde se testuje ztráta zobrazovacího zařízení a zajišťuje se náprava. Při opětovné inicializaci se kromě uvolňování vertex bufferu uvolňuje i index buffer:
4. Direct3D
Index buffer se vytváří voláním metody IDirect3DDevice9::CreateIndexBuffer, jež má šest parametrů. Prvním parametrem je požadovaná velikost bufferu v bytech, druhý parametr udává způsob použití – nula znamená, že způsob použití nechceme specifikovat. Třetí parametr (a to je rozdíl proti vytváření vertex bufferu) je datový typ D3DFORMAT. My jsme si dříve uvedli, že tento datový typ udává barevný režim obrazovky. Jeho využití je ovšem širší. Pro buffery se používají specifické hodnoty udávající jejich formát. U nás přicházejí v úvahu dvě, lišící se bitovou hloubkou: D3DFMT_INDEX16 a D3DFMT_INDEX32. Poslední tři parametry jsou opět stejné jako při vytváření vertex bufferu. Čtvrtý argument tedy udává typ použité paměti, pátý adresu, kam se uloží ukazatel na rozhraní vytvořeného index bufferu, poslední parametr se nepoužívá a je vždy NULL.
if((d3dval=m_lpD3DDevice->TestCooperativeLevel()) != D3D_OK) { if(d3dval == D3DERR_DEVICELOST) return; if(d3dval == D3DERR_DEVICENOTRESET) { RELEASE_DX_OBJECT(m_lpD3DIndexBuffer); RELEASE_DX_OBJECT(m_lpD3DVertexBuffer); if((m_lpD3DDevice->Reset(&m_D3Dpp)) != D3D_OK) { sprintf(g_tcError,TEXT("Nelze obnovit zařízení!"));
4.6 Práce s index bufferem
Prodáno 14.10.2014 na www.Kosmas.cz zákazníkovi [email protected], číslo objednávky 591522, UID: 7e15c933-e4b8-4e2a-94a6-c3fd2dffdec9
118
DIRECTX
PostQuitMessage(0); return; } Sleep(100); if(!CreateSources()) { PostQuitMessage(0); return; } } } Do zbylé části (uvnitř bloku, kde se vypisuje na obrazovku) se za nastavení vertex bufferu musí nastavit i index buffer. To provádí metoda IDirect3DDevice9::SetIndices, která má jediný parametr, a sice ukazatel na index buffer, jenž se má pro vykreslení nastavit: if((m_lpD3DDevice->SetIndices(m_lpD3DIndexBuffer)) != D3D_OK) { sprintf(g_tcError,TEXT("Nelze nastavit vstupní index buffer!")); PostQuitMessage(0); } Poslední změna v metodě CWindow::UpdateFrame a vlastně i v celém programu se postará o vykreslení. Tentokrát se však místo metody IDirect3DDevice9::DrawPrimitive volá IDirect3DDevice9::DrawIndexedPrimitive: if((m_lpD3DDevice->DrawIndexedPrimitive(D3DPT_LINELIST, 0, 0, NUMBER_OF_INDEXES, 0, NUMBER_OF_INDEXES/2)) != D3D_OK) { sprintf(g_tcError,TEXT("Nelze vykreslit objekty!")); PostQuitMessage(0); } První parametr této metody udává, co se má vykreslovat. K dispozici jsou stejné typy objektů jako u vykreslování pomocí vertex bufferu. Naše krychle se bude skládat z jednoduchých úseček, proto zde máme uvedeno D3DPT_LINELIST. Druhým parametrem je posun od začátku bufferu, kde začínají indexy vertexů, které se mají vykreslovat. My vykreslujeme od začátku, proto je zde uvedena nula. Jako třetí parametr máme také nulu. Tento argument udává minimální index vertexu, který se používá. Potom následuje počet vertexů, jež se při vykreslení používají (u krychle je to 24). Předposlední parametr je index v index bufferu, od něhož se budou brát údaje. Takto můžeme zajistit, že se nebudou vykreslovat objekty dané počátečními indexy. Poslední argument udává počet objektů, jež se mají vykreslit. Úsečka je dána dvěma body, proto je to poloviční hodnota z počtu indexů v index bufferu.
4.7 Transformace V předchozích příkladech byly objekty definované pomocí vertexů, jejichž souřadnice odpovídaly přímo pixelům na obrazovce. Takto se ovšem souřadnice v drtivé většině případů nezadávají. V praxi bývá poloha vertexů dána vzhledem k jakémusi středovému bodu, který bývá obvykle v těžišti tohoto objektu (často se také říká, že každý objekt má svůj lokální souřadnicový systém s počátkem ve středovém bodě). V takovém případě se musí
4. Direct3D
Prodáno 14.10.2014 na www.Kosmas.cz zákazníkovi [email protected], číslo objednávky 591522, UID: 7e15c933-e4b8-4e2a-94a6-c3fd2dffdec9
DIRECTX
119
na tyto souřadnice aplikovat transformace, na jejichž výstupu opět získáváme souřadnice na obrazovce. K transformacím se používají matice (viz podkapitolu 4.1.3). Tyto transformace jsou celkem trojího druhu. První z nich je transformace tzv. světovou maticí, kdy se převádí lokální souřadnice na globální. Světový souřadnicový systém představuje celou scénu se všemi objekty, které se v ní nacházejí, přičemž touto transformací dosáhneme toho, že souřadnice všech vertexů všech objektů budou vztaženy ke společnému počátku. Potom se provádí druhé transformace – pohledové. Zde se definuje pohled kamery v globálním souřadnicovém systému pomocí směru os kamery, její polohy a souřadnic bodu, do kterého směřuje její pohled. Kamera představuje místo, odkud se na scénu díváme, její směr udává směr pohledu. Poslední transformace jsou projekční. Ty definují způsob zobrazení – zpravidla perspektivu, tedy způsob zobrazení odpovídající lidskému vnímání. V praxi to znamená, že bližší objekty jsou větší a naopak vzdálenější objekty menší, rovnoběžné okraje rovné cesty se směrem do dálky sbíhají apod. Při této transformaci se nastavuje poměr stran pozorovaného prostoru, zorný úhel kamery a prostor, který kamera „vidí“. Abychom tyto transformace co nejlépe demonstrovali na příkladu, vybrali jsme úplně jednoduchou aplikaci s jedním trojúhelníkem, který se bude na obrazovce otáčet (jak si uvedeme dále, tak s malou úpravou se trojúhelník může klidně i posouvat nebo měnit svoji velikost). Samozřejmě si můžeme program upravit i pro transformace složitějších objektů nebo například umístit obrázek na pozadí – to jsou všechno věci, které jsme probrali v předchozích částech. My ale vyjdeme z programu z podkapitoly 4.5 a popíšeme si příslušné úpravy, které na zmíněný program použijeme. Začneme souborem Global.h. Zde vložíme navíc hlavičkový soubor d3dx9.h a přilinkujeme odpovídající knihovnu: #include
Důvodem je skutečnost, že při transformacích budeme pracovat s maticemi. Pro snazší práci je v souboru d3dx9.h k dispozici datový typ D3DXMATRIX, který tuto činnost velice usnadňuje. Proto ho v našem programu několikrát použijeme. Dále v souboru Global.h dojde ke změnám hodnot dvou symbolických konstant:
4. Direct3D
#pragma comment (lib,"d3dx9.lib")
#define NUMBER_OF_VERTICES 3 #define D3DFVF_CUSTOMVERTEX (D3DFVF_XYZ|D3DFVF_DIFFUSE) Makro NUMBER_OF_VERTICES udává počet vertexů a protože budeme vykreslovat jen jeden trojúhelník, vertexy budou tři. Pomocí makra D3DFVF_CUSTOMVERTEX jsme udávali formát vertexů. Zatímco jsme ponechali příznak D3DFVF_DIFFUSE, novinkou je příznak D3DFVF_XYZ (dříve jsme používali D3DFVF_XYZRHW). Příznak D3DFVF_XYZ říká, že souřadnice u vertexů, které tvoří zobrazované objekty, nejsou transformované. Jinými slovy, dříve jsme v souřadnicích vertexů měli přímo souřadnice pixelů na obrazovce, zde ovšem budeme mít „nějaké“ souřadnice, které – aby se staly transformovanými – musí projít transformacemi, jež jsme popsali na začátku této kapitoly. Poslední úpravou v souboru Global.h se týká symbolické konstanty WAITING_FOR_ FLIP: #define WAITING_FOR_FLIP
50
4.7 Transformace
Prodáno 14.10.2014 na www.Kosmas.cz zákazníkovi [email protected], číslo objednávky 591522, UID: 7e15c933-e4b8-4e2a-94a6-c3fd2dffdec9
120
DIRECTX
V předchozích programech jsme do tohoto makra ukládali počet milisekund, které udávaly, jak často se volá hlavní cyklus programu. Dosud jsme v Direct3D používali statický obraz – nic se v něm nepohybovalo, proto na této hodnotě příliš nezáleželo. Nyní, když se bude trojúhelník na obrazovce pohybovat, je situace jiná. Hodnotou tohoto makra ovlivňujeme i rychlost pohybu trojúhelníku. Čím bude hodnota tohoto makra menší, tím bude pohyb rychlejší, a naopak. Do souborů WinMain.cpp, CApplication.h a CApplication.cpp nebudeme vůbec zasahovat. Další změny se tedy budou týkat výhradně třídy CWindow a jejích metod. Ještě předtím, než se zaměříme na třídu CWindow, podívejme se na formát struktury vertexu, který rovněž prošel změnou: struct CUSTOMVERTEX { float x, y, z; DWORD color; }; Tuto strukturu potkalo zjednodušení, odstranili jsme váhu bodu. Zde s touto hodnotou nebudeme pracovat, protože pro ni transformační vzorce, které budeme ze souboru d3dx9.h používat, nejsou stavěny. Kdybychom tuto hodnotu ve struktuře ponechali a naplňovali ji hodnotou 1, po spuštění programu bychom zjistili, že tato hodnota již ovlivňuje barvu. Potom by barvy, které bychom definovali v položce color, neodpovídaly barvám na obrazovce. Ve třídě CWindow zůstaly zachovány všechny metody, přibyl zde pouze jeden atribut: D3DXMATRIX m_matWorld; Jde o světovou matici. Důvodem, proč ji máme mezi soukromými atributy, je fakt, že si potřebujeme pamatovat její hodnoty, aby se v každém následujícím cyklu správně počítaly nové transformované souřadnice. V konstruktoru třídy CWindow došlo k následujícím změnám počátečních inicializací: m_aVertices[0].x = 0.0f; m_aVertices[0].y = 0.0f; m_aVertices[0].z = 0.0f; m_aVertices[0].color=D3DCOLOR_XRGB(255,0,0); m_aVertices[1].x = 0.0f; m_aVertices[1].y = 0.5f; m_aVertices[1].z = 0.0f; m_aVertices[1].color=D3DCOLOR_XRGB(0,255,0); m_aVertices[2].x = 0.5f; m_aVertices[2].y = 0.0f; m_aVertices[2].z = 0.0f; m_aVertices[2].color=D3DCOLOR_XRGB(0,0,255); D3DXMatrixTranslation(&m_matWorld, 0.0f, 0.0f, 0.0f); Jako první plníme pole tří vertexů. Všimněte si jednak barev (abychom vertexy poznali snadno na obrazovce, mají přiřazenou barvu červenou, zelenou a modrou) a jednak jejich souřadnic. Jak víme, tyto souřadnice nepředstavují souřadnice na obrazovce, ty teprve získají až po transformacích. Pochopitelně můžete souřadnice různě upravovat, jen je
4. Direct3D
Prodáno 14.10.2014 na www.Kosmas.cz zákazníkovi [email protected], číslo objednávky 591522, UID: 7e15c933-e4b8-4e2a-94a6-c3fd2dffdec9
DIRECTX
121
třeba dát pozor na pořadí vrcholů. Připomeňme, že pořadí vertexů musí být ve směru hodinových ručiček, jinak by trojúhelník byl zobrazen obráceně a nebyl by na obrazovce vůbec vidět. Dále inicializujeme světovou matici funkcí D3DXMatrixTranslation. Tato funkce představuje transformaci posunu, který se aplikuje na matici, jež je dána ukazatelem, což je první argument této funkce. Zbylé tři parametry představují posuny ve směru os x, y a z. Když už jsme u transformace posunu, na chvíli od hlavního tématu odbočme a udělejme si přehled funkcí pro všechny základní transformace: D3DXMATRIX* D3DXMatrixTranslation(D3DXMATRIX * pOut, FLOAT x, FLOAT y, FLOAT z); D3DXMATRIX* D3DXMatrixScaling(D3DXMATRIX *pOut, FLOAT sx, FLOAT sy, FLOAT sz); D3DXMATRIX* D3DXMatrixRotationX(D3DXMATRIX *pOut, FLOAT Angle); D3DXMATRIX* D3DXMatrixRotationY(D3DXMATRIX *pOut, FLOAT Angle); D3DXMATRIX* D3DXMatrixRotationZ(D3DXMATRIX *pOut, FLOAT Angle); Z názvu těchto funkcí je patrné, k čemu slouží. Funkci posunu jsme si již popsali. Stejné parametry má funkce pro změnu měřítka (D3DXMatrixScaling), pouze hodnoty sx, sy a sz udávají změnu měřítka ve směrech příslušných os. Následující tři funkce aplikují transformaci otáčení kolem tří os. První parametr těchto funkcí je opět ukazatel na matici, na kterou se otáčení aplikuje, druhým parametrem je úhel, o který se má provést otočení. Tento úhel je v radiánech.
m_D3Dpp.Windowed = false; m_D3Dpp.SwapEffect = D3DSWAPEFFECT_DISCARD; m_D3Dpp.BackBufferFormat = m_D3DFormat; m_D3Dpp.BackBufferWidth = SCREEN_WIDTH; m_D3Dpp.BackBufferHeight = SCREEN_HEIGHT; m_D3Dpp.hDeviceWindow = m_hWnd; // m_D3Dpp.MultiSampleType = D3DMULTISAMPLE_4_SAMPLES; if((m_lpD3D->CreateDevice(0, D3DDEVTYPE_HAL, m_hWnd, D3DCREATE_SOFTWARE_VERTEXPROCESSING, &m_D3Dpp, &m_lpD3DDevice)) != D3D_OK) { sprintf(g_tcError,TEXT("Nelze vytvořit objekt Direct3DDevice!")); return (false); }
4. Direct3D
Vraťme se zpět k popisu programu. V těle konstruktoru již další změny nejsou. Úpravy se nedotkly ani destruktoru, ani metody CWindow::Terminate. V metodě CWindow::Initialize došlo k jedné drobné změně při nastavování vlastnosti zařízení.
Tato změna se netýká transformací, nýbrž antialiasingu, neboli vyhlazování hran. V naší aplikaci se bude otáčet trojúhelník a pokud antialiasing nezapneme, můžeme si všimnout, že hrany trojúhelníku jsou v závislosti na jejich sklonu „zubaté“. Antialiasing se dá zapnout položkou m_D3Dpp.MultiSampleType. Hodnotu tohoto argumentu jsme dříve nenastavovali. Jinými slovy: zde měla hodnotu 0, což znamená, že antialiasing je vypnutý. Obecně však sem můžeme dosadit D3DMULTISAMPLE_x_SAMPLES, kde za x v této symbolické konstantě uprostřed můžete dosadit čísla v rozmezí 2–16. Čím vyšší použijete číslo, tím je vyhlazování kvalitnější. Ovšem také časově náročnější a navíc je ovlivněno i možnostmi grafické karty. A to je důvod, proč je umístěn tento parametr v komentáři,
4.7 Transformace
Prodáno 14.10.2014 na www.Kosmas.cz zákazníkovi [email protected], číslo objednávky 591522, UID: 7e15c933-e4b8-4e2a-94a6-c3fd2dffdec9
122
DIRECTX
čili v programu se standardně vyhlazování nezapíná. Zkuste ale experimentovat, jaké kvality vyhlazování vaše grafická karta může dosáhnout. Při překročení hodnoty, kterou je schopna grafická karta v počítači dosáhnout, dojde po spuštění programu k chybě – vypíše se zpráva Nelze vytvořit objekt Direct3DDevice!. Osobně jsem na kartě Radeon 9600 vyzkoušel, že maximální hodnota je D3DMULTISAMPLE_4_SAMPLES, což je uvedeno v komentáři našeho programu. Natavení všech tří transformací (světové, pohledové a projekční) provádíme v metodě CWindow::CreateSources. Důvodem je, že při ztrátě zařízení musíme tyto transformace opět nastavit. Zde je ta část programu, která v metodě CWindow::CreateSources přibyla: D3DXVECTOR3 vEyePt(0.0f, 0.0f, -2.0f); D3DXVECTOR3 vLookatPt(0.0f, 0.0f, 0.0f); D3DXVECTOR3 vUpVec(0.0f, 1.0f, 0.0f); D3DXMATRIX viewMatrix; D3DXMATRIX projectionMatrix; D3DXMatrixTranslation(&m_matWorld, 0, 0, 0); m_lpD3DDevice->SetTransform(D3DTS_WORLD, &m_matWorld); D3DXMatrixLookAtLH(&viewMatrix, &vEyePt, &vLookatPt, &vUpVec); m_lpD3DDevice->SetTransform(D3DTS_VIEW, &viewMatrix); D3DXMatrixPerspectiveFovLH(&projectionMatrix, D3DX_PI/4, 4.0f/3.0f, 1.0f, 50.0f); m_lpD3DDevice->SetTransform(D3DTS_PROJECTION, &projectionMatrix); m_lpD3DDevice->SetRenderState(D3DRS_LIGHTING, FALSE); Prozatím přeskočme počáteční deklarace, vrátíme se k nim ve chvíli, kdy o nich budeme hovořit u jednotlivých funkcí. Jako první si můžeme všimnout volání funkce D3DXMatrixTranslation, kterou jsme si již popsali. Je to stejný příkaz, jaký máme v konstruktoru, a protože posunujeme „nikam“, je volání této funkce zbytečné. Je tu však vloženo úmyslně, abyste mohli zkusit experimentovat s posuny ve směru jednotlivých os, tedy zkusili měnit hodnoty. Jenom je nenastavujte příliš vysoké, protože pak by se trojúhelník nacházel někde mimo oblast pohledu kamery. V takovém případě by nebyl vidět. Dále následuje volání metody IDirect3DDevice9::SetTransform. Tato metoda nastavuje transformace. Všimněme si, že tuto metodu voláme v našem kódu celkem třikrát s různými parametry. Obecně platí, že první parametr udává typ transformace a druhý je ukazatel na transformační matici, jejíž hodnoty se při transformaci používají. První parametry máme v tomto pořadí: D3DTS_WORLD (světová matice), D3DTS_VIEW (pohledová matice) a D3DTS_PROJECTION (projekční matice). Před nastavením pohledové matice voláme funkci D3DXMatrixLookAtLH, která má za úkol právě tuto matici sestavit. Sestavení se provádí na základě čtyř parametrů této funkce. První parametr je ukazatel na matici, kam se mají uložit data transformace. Dále následuje ukazatel na vektor udávající polohu kamery ve scéně, tedy místo, odkud budeme celou scénu sledovat (&vEyePt). Hodnotami uloženými v tomto vektoru můžeme samozřejmě polohu kamery měnit. Třetí argument (&vLookatPt) je opět ukazatel na vektor, který ale tentokrát udává polohu cílového bodu kamery, tedy místo, kam se bude kamera dívat. Poslední parametr (&vUpVec) je opět ukazatel na vektor, který určuje, jakým způsobem bude kamera nakloněna vzhledem k ose pohledu.
4. Direct3D
Prodáno 14.10.2014 na www.Kosmas.cz zákazníkovi [email protected], číslo objednávky 591522, UID: 7e15c933-e4b8-4e2a-94a6-c3fd2dffdec9
DIRECTX
123
Nastavení projekční matice provádí funkce D3DXMatrixPerspectiveFovLH. Také zde udává první parametr ukazatel na matici, kam se budou ukládat data této transformace, zbylé parametry udávají samotné nastavení. První je zorný úhel kamery, který se zadává v radiánech, potom následuje poměr stran kamery (my máme nastaveno 4 : 3) a poslední dva argumenty udávají oblast, kterou kamera vidí. Konkrétně se jedná o vzdálenosti kamery, kde je rovina řezu, dále pak to, co bude před ní a nebude tedy vidět (předposlední parametr) a rovina řezu, kde nebude vidět to, co je za ní (poslední parametr). Nakonec ještě voláním metody IDirect3DDevice9::SetRenderState vypínáme osvětlení scény. Problematika práce se světly je poněkud rozsáhlejší, proto se o ní dozvíme více v samostatné podkapitole 4.10. Světla vypínáme, protože je nyní nepotřebujeme – samotný trojúhelník bude emitovat barvu danou barvami ve vrcholech (viz symbolickou konstantu D3DFVF_CUSTOMVERTEX v souboru Global.h). Metoda IDirect3DDevice9::SetRenderState má však širší využití. Jejím obecným smyslem je nastavit vlastnosti pro renderování. Jaká vlastnost to má být, to udává první parametr. Druhý parametr této funkce představuje hodnotu, kterou příslušnou vlastnost nastavujeme. S různými parametry této metody se setkáme v dalších podkapitolách. Poslední změny v programu se týkají metody CWindow::UpdateFrame. Hned za úvodní částí, kde se testuje případná ztráta zařízení a náprava, následuje tento programový kód: m_lpD3DDevice->Clear(0, NULL, D3DCLEAR_TARGET, D3DCOLOR_XRGB(255, 255, 255), 1.0f, 0); D3DXMatrixRotationZ(&matRot, 1*(D3DX_PI/180.0f)); D3DXMatrixMultiply(&m_matWorld, &m_matWorld, &matRot); // D3DXMatrixRotationX(&matRot, 1*(D3DX_PI/180.0f)); // D3DXMatrixMultiply(&m_matWorld, &m_matWorld, &matRot);
Nejprve překreslíme obrazovku, tentokrát bílou barvou, a poté se podle našeho požadavku aplikují transformace, které mají způsobit pohyb na obrazovce. V našem případě otáčíme trojúhelník kolem osy z, ale v uvedeném kódu je v komentáři i část, pomocí níž můžeme přidat otáčení trojúhelníku zároveň i kolem osy x. U těchto funkcí ještě uveďme, že (D3DX_PI/180.0f) je přepočet stupňů na radiány. Při každém zavolání funkce UpdateFrame se trojúhelník otočí o jeden stupeň. Správně bychom ale neměli říkat, že otáčíme trojúhelníkem. Tato transformace by se totiž automaticky aplikovala na všechny objekty, které bychom do scény umístili.
4. Direct3D
m_lpD3DDevice->SetTransform(D3DTS_WORLD, &m_matWorld);
Dosud neznámou je pro nás funkce D3DXMatrixMultiply, jež násobí matice a pomocí ní tak můžeme transformace skládat (například kombinovat otáčení a posun). Jen je potřeba dbát na správné skládání transformací. Jak jsme si uvedli již dříve, při chybné posloupnosti transformací získáme i chybné výsledky. Funkce D3DXMatrixMultiply má trojici parametrů, kde první představuje ukazatel na matici, kam se uloží výsledek násobení, a zbylé dva parametry jsou ukazatele na matice, které se společně násobí v pořadí odpovídajícím pořadí parametrů. V našem programu si uchováváme matici m_matWorld (atribut třídy CWindow), kterou cyklicky násobíme maticí rotace kolem osy z. Vyřešit by se to dalo i bez uchovávání matice m_matWorld, neboť existuje metoda IDirect3DDevice9::GetTransform, jež dokáže nastavené transformace zpětně zjistit. Takto by mohlo vypadat její volání:
4.7 Transformace
Prodáno 14.10.2014 na www.Kosmas.cz zákazníkovi [email protected], číslo objednávky 591522, UID: 7e15c933-e4b8-4e2a-94a6-c3fd2dffdec9
124
DIRECTX
D3DXMATRIX matWorld; if((m_lpD3DDevice->GetTransform(D3DTS_WORLD, &matWorld)) != D3D_OK) { sprintf(g_tcError,TEXT("Nelze získat transformaci!")); PostQuitMessage(0); } Prvním parametrem je typ transformační matice, kterou chceme získat, druhým je pak ukazatel na matici, kam se mají získané údaje uložit. Po získání této matice bychom již postupovali stejně, tedy tuto matici bychom násobili maticí otáčení a nové transformace bychom metodou IDirect3DDevice9::SetTransform nastavili. Tímto bychom se zbavili ve třídě CWindow soukromého atributu m_matWorld. Pokud chcete, můžete tuto úpravu do programu implementovat. Není to příliš složité. Poslední částí úpravy v metodě CWindow::UpdateFrame je vykreslení trojúhelníku pomocí metody IDirect3DDevice9::DrawPrimitive. Přesné umístění této metody v metodě CWindow::UpdateFrame i specifikace jejích parametrů je shodné jako v předchozích příkladech, proto není nutné uvádět podrobnější popis: if((m_lpD3DDevice->DrawPrimitive(D3DPT_TRIANGLELIST, 0, 1)) != D3D_OK) { sprintf(g_tcError,TEXT("Nelze vykreslit objekty!")); PostQuitMessage(0); }
4.8 Práce se složitějšími objekty a formát .x V předchozích podkapitolách jsme zobrazovali pouze ty nejjednodušší objekty, které měly několik vertexů. Pokud jste hráli nějakou 3D hru, tak objekty, jež se ve scénách hry objevují, mají těchto vertexů stovky, tisíce, někdy i mnohem více. Samozřejmě by v takových případech bylo značně neefektivní zadávat vertexy do paměti počítače tak, jak jsme si ukázali dříve. Proto existují různé formáty souborů, kde jsou všechny potřebné informace pro práci s takovými objekty uloženy. V podstatě každý 3D grafický editor, v němž se takové modely vytvářejí, má svůj vnitřní formát. Protože je však potřeba modely přenášet i do jiných aplikací, existují formáty, které jsou podporovány velkým množstvím modelovacích programů. Zároveň je pro tyto formáty k dispozici i podpora funkcí pro snadnou implementaci do vlastních programů. Patrně nejčastěji používané takové formáty jsou *.x *.3ds a *.ase. My se v této podkapitole (a vlastně i v celé knize) zaměříme pouze na formát *.x, který je ze strany DirectX plně podporován. Pokud tedy nějaký 3D model vytvoříte v některém z grafických programů jako je 3D Studio Max nebo Blender, stačí ho exportovat právě do formátu *.x, jedinou zavolanou funkcí ho načíst, získat z něj všechny potřebné informace a poté zobrazit. Tuto činnost bude provádět program, který si nyní podrobně popíšeme. Zobrazíme a necháme rotovat jednoduchou věž, jejíž drátové zobrazení (nejsou tedy zatím vidět plochy) máme znázorněné na obrázku 4.10. Velice jednoduchou úpravou si takto můžete nechat zobrazit libovolný jiný objekt ve formátu *.x. Vyjdeme z programu, který jsme si popsali v předchozí kapitole. Nepotřebujeme zde ovšem žádný vertex buffer, proto z programu vymažeme všechny příkazy, které se ho týkaly (to
4. Direct3D
Prodáno 14.10.2014 na www.Kosmas.cz zákazníkovi [email protected], číslo objednávky 591522, UID: 7e15c933-e4b8-4e2a-94a6-c3fd2dffdec9
DIRECTX
125
ale neznamená, že program žádný buffer nepoužívá, pouze se o něj nyní nemusíme starat). Naopak k šestici původních souborů přibude dvojice CMesh.h a CMesh.cpp s třídou CMesh a jejími metodami, které se budou starat o načítaný mesh objekt. Pro nepotřebnost nejsou nyní v souboru Global.h symbolické konstanty NUMBER_OF_VERTICES a D3DFVF_CUSTOMVERTEX. Naopak zde přibyla makra MESHFILE a RELEASE_OBJECT. Prvně jmenované makro udává jméno souboru, ze kterého budeme objekt načítat, druhé slouží pro uvolňování dynamických instancí: #define MESHFILE TEXT("Tower.x") #define RELEASE_OBJECT(p) {if(p){delete p; p = NULL;};}; Soubory WinMain.cpp, CWindow.h a CWindow.cpp zůstaly nedotčeny. Prozatím přeskočme soubory CWindow.h a CWindow.cpp a pojďme prozkoumat třídu CMesh. Ve zmíněných dvou souborech ke změnám samozřejmě došlo, ale protože třída CWindow agreguje instanci třídy CMesh, výklad bude takto názornější. V souboru CMesh.h je třída CMesh deklarována následovně: Obrázek 4.10: Drátový model věže
#pragma once class CMesh{ private: LPD3DXMESH m_lpD3DXMesh;
CMesh(void); ~CMesh(void); LPD3DXMESH GetMesh() const {return m_lpD3DXMesh;}
4. Direct3D
public:
bool Initialize(TCHAR *FileName, LPDIRECT3DDEVICE9 lpDevice); void Terminate(void); }; Tělo této třídy není příliš rozsáhlé, ale pro naše aktuální potřeby bude stačit. Ve třídě je pouze jediný atribut m_lpD3DXMesh (typ LPD3DXMESH). LPD3DXMESH je ukazatel na rozhraní typu ID3DXMesh a poté, co náš model načteme, skrze m_lpD3DXMesh k němu budeme moci přistupovat. Potom zde máme konstruktor, destruktor a metodu Terminate (bude volána při konci platnosti instance). Metoda GetMesh pouze vrací ukazatel na mesh objekt a poslední členská metoda Initialize načítá objekt ze souboru. Těla konstruktoru, destruktoru a metody CMesh::Terminate vypadají následovně. Protože neobsahují pro nás nic, co bychom dosud nepopsali, není třeba k nim uvádět bližší komentáře:
4.8 Práce se složitějšími objekty a formát .x
Prodáno 14.10.2014 na www.Kosmas.cz zákazníkovi [email protected], číslo objednávky 591522, UID: 7e15c933-e4b8-4e2a-94a6-c3fd2dffdec9
126
DIRECTX
CMesh::CMesh(void) { m_lpD3DXMesh = NULL; }
CMesh::~CMesh(void) { Terminate(); } void CMesh::Terminate(void) { RELEASE_DX_OBJECT(m_lpD3DXMesh); } Uvedli jsme si, že metoda CMesh::Initialize načítá 3D grafický model ze souboru. Jméno souboru je prvním parametrem této metody, druhým parametrem je ukazatel na rozhraní zobrazovacího zařízení, neboť ho v těle také potřebujeme: bool CMesh::Initialize(TCHAR *FileName, LPDIRECT3DDEVICE9 lpDevice) { if(lpDevice) { if((D3DXLoadMeshFromX(FileName, 0, lpDevice, NULL, NULL, NULL, NULL, &m_lpD3DXMesh)) != D3D_OK) { sprintf(g_tcError,TEXT("Nelze načíst model ze souboru!")); return (false); } return (true); } sprintf(g_tcError,TEXT("Nelze načíst model. Neexistuje objekt Direct3DDevice!")); return (false); } Načítání modelu ze souboru *.x je otázkou volání jedné funkce: D3DXLoadMeshFromX. Tato funkce má celkem osm parametrů, z nichž ty, které nás nezajímají, mají hodnotu 0 nebo NULL. První argument je ukazatel na řetězec s názvem souboru, ze kterého se má soubor načíst. Dále následuje parametr, který je kombinací příznaků, jež udávají vlastnosti vytvořeného mesh objektu. Třetí je ukazatel na rozhraní zařízení IDirect3DDevice9, které bude s mesh objektem asociováno. Následuje čtveřice parametrů, jež se naplní daty po načtení objektu. Je to ukazatel na buffer, který obsahuje specifikace sousedních ploch, ukazatel na buffer s materiálovými údaji, ukazatel na buffer, kde bude uloženo pole instancí efektů, které se budou na mesh objekt aplikovat, a ukazatel na počet materiálů. Poslední parametr je adresa ukazatele na objekt rozhraní ID3DXMesh, který bude reprezentovat mesh objekt. Při úspěšném volání funkce D3DXLoadMeshFromX se vrací hodnota D3D_OK. Nyní se můžeme vrátit ke třídě CWindow (soubor CWindow.h):
4. Direct3D
Prodáno 14.10.2014 na www.Kosmas.cz zákazníkovi [email protected], číslo objednávky 591522, UID: 7e15c933-e4b8-4e2a-94a6-c3fd2dffdec9
DIRECTX
127
#pragma once #include "CMesh.h" class CWindow { private: HWND m_hWnd; LPDIRECT3D9 m_lpD3D; LPDIRECT3DDEVICE9 m_lpD3DDevice; D3DPRESENT_PARAMETERS m_D3Dpp; D3DFORMAT m_D3DFormat; CMesh * m_lpMesh; public: CWindow(void); ~CWindow(void); bool Initialize(void); void Terminate(void); bool CreateSources(void); void UpdateFrame(void); friend LRESULT CALLBACK WndProc(HWND m_hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam); Na začátku vkládáme hlavičkový soubor CMesh.h. Je to kvůli novému členskému atributu CMesh *m_lpMesh – tato instance bude představovat náš mesh objekt. Dále byl odstraněn ukazatel na rozhraní pro vertex buffer a také matice D3DXMATRIX m_matWorld (), protože otáčení vyřešíme přes metodu m_lpD3DDevice->GetTransform, jak jsme si popsali na konci předchozí kapitoly.
4. Direct3D
};
Úměrně změnám atributů třídy CWindow se mění i příkazy v metodách této třídy v souboru CWindow.cpp. Těla konstruktoru, destruktoru a metody CWindow::Terminate vypadají nyní následovně: CWindow::CWindow(void) { m_hWnd = NULL; m_lpD3D = NULL; m_lpD3DDevice = NULL; m_lpMesh = NULL; ZeroMemory(&m_D3Dpp, sizeof(m_D3Dpp)); m_D3DFormat = (SCREEN_WIDTH == 32) ? D3DFMT_X8R8G8B8 : D3DFMT_R5G6B5; }
4.8 Práce se složitějšími objekty a formát .x
Prodáno 14.10.2014 na www.Kosmas.cz zákazníkovi [email protected], číslo objednávky 591522, UID: 7e15c933-e4b8-4e2a-94a6-c3fd2dffdec9
128
DIRECTX
CWindow::~CWindow(void) { Terminate(); } void CWindow::Terminate(void) { RELEASE_OBJECT(m_lpMesh); RELEASE_DX_OBJECT(m_lpD3DDevice); RELEASE_DX_OBJECT(m_lpD3D); CloseWindow(m_hWnd); } V konstruktoru nastavujeme ukazatel m_lpMesh na NULL (objekt ještě vytvořený není) a v destruktoru, nebo přesněji v metodě Terminate, pokud tento objekt existuje, ho zrušíme zavoláním makra RELEASE_OBJECT. K vytvoření objektu m_lpMesh dochází na konci metody CWindow::Initialize: m_lpMesh = new CMesh; if(!m_lpMesh->Initialize(MESHFILE, m_lpD3DDevice)) return (false); Jinak v metodě CWindow::Initialize k dalším úpravám nedošlo. Několika změnami ovšem prošla metoda CWindow::CreateSources: bool CWindow::CreateSources(void) { D3DXVECTOR3 vEyePt(0.0f, 10.0f, -15.0f); D3DXVECTOR3 vLookatPt(0.0f, 5.0f, 0.0f); D3DXVECTOR3 vUpVec(0.0f, 1.0f, 0.0f); D3DXMATRIX viewMatrix; D3DXMATRIX projectionMatrix; D3DXMATRIX matWorld; D3DXMatrixTranslation(&matWorld, 0, 0, 0); m_lpD3DDevice->SetTransform(D3DTS_WORLD, &matWorld); D3DXMatrixLookAtLH(&viewMatrix, &vEyePt, &vLookatPt, &vUpVec); m_lpD3DDevice->SetTransform(D3DTS_VIEW, &viewMatrix); D3DXMatrixPerspectiveFovLH(&projectionMatrix, D3DX_PI/4, 4.0f/3.0f, 1.0f, 50.0f); m_lpD3DDevice->SetTransform(D3DTS_PROJECTION, &projectionMatrix); m_lpD3DDevice->SetRenderState(D3DRS_FILLMODE, D3DFILL_WIREFRAME); m_lpD3DDevice->SetRenderState(D3DRS_CULLMODE, D3DCULL_NONE); return (true); }
4. Direct3D
Prodáno 14.10.2014 na www.Kosmas.cz zákazníkovi [email protected], číslo objednávky 591522, UID: 7e15c933-e4b8-4e2a-94a6-c3fd2dffdec9
DIRECTX
129
Ke změnám došlo hned v prvních dvou definicích. Jak víme, vEyePt představuje vektor polohy, odkud se na scénu budeme dívat (kde je umístěna kamera). Protože je věž vyšší, je kamera umístěna trochu výše (vyšší hodnota y-ové souřadnice) a dále od počátku (vyšší hodnota z-ové souřadnice). Úměrně tomu jsme upravili i y-ovou souřadnici u vektoru vLookatPt, tedy cílový bod, kam se kamera dívá. Na konci metody CWindow::CreateSources je dvojice příkazů m_lpD3DDevice>SetRenderState, jež zapínají plné drátové zobrazení: věž se bude vykreslovat tak, jak je uvedeno na obrázku 4.10. Pokud vložíme oba příkazy do komentářů, budou se vykreslovat všechny vyplněné plochy. Protože ale zatím nemáme definovány materiálové vlastnosti a neprovádíme ani žádné konkrétní nastavení pro světla (oboje bude náplní následujících podkapitol), všechny plochy budou černé. S příkazem m_lpD3DDevice->SetRenderState jsme se již setkali v předchozí kapitole. Vypínali jsme jeho pomocí světlo, ale uvedli jsme si, že jeho použití je širší. První příkaz (první parametr funkce je D3DRS_FILLMODE) zapíná drátové zobrazení bez kreslení ploch, druhý (první parametr funkce je D3DRS_CULLMODE) zajišťuje vykreslování bez ohledu na pořadí vertexů v bufferu. Jak víme, pokud je pořadí v bufferu špatné, standardně se nic nevykreslí. Pokud zkusíme tento příkaz dát do komentáře, zjistíme, že se nebudou vykreslovat odvrácené hrany. V poslední metodě CWindow::UpdateFrame došlo k více úpravám, proto si ji uveďme celou: void CWindow::UpdateFrame(void) { HRESULT d3dval; D3DXMATRIX matWorld; D3DXMATRIX matRot;
if(d3dval == D3DERR_DEVICENOTRESET) { RELEASE_OBJECT(m_lpMesh); if((m_lpD3DDevice->Reset(&m_D3Dpp)) != D3D_OK) { sprintf(g_tcError,TEXT("Nelze obnovit zařízení!")); PostQuitMessage(0); return; }
4. Direct3D
if((d3dval=m_lpD3DDevice->TestCooperativeLevel()) != D3D_OK) { if(d3dval == D3DERR_DEVICELOST) return;
Sleep(100); if(!CreateSources()) PostQuitMessage(0); m_lpMesh = new CMesh; if(!m_lpMesh->Initialize (MESHFILE, m_lpD3DDevice))
4.8 Práce se složitějšími objekty a formát .x
Prodáno 14.10.2014 na www.Kosmas.cz zákazníkovi [email protected], číslo objednávky 591522, UID: 7e15c933-e4b8-4e2a-94a6-c3fd2dffdec9
130
DIRECTX
PostQuitMessage(0); return; } } if(m_lpD3DDevice) { m_lpD3DDevice->Clear(0, NULL, D3DCLEAR_TARGET, D3DCOLOR_XRGB(255, 255, 255), 1.0f, 0); if((m_lpD3DDevice->GetTransform(D3DTS_WORLD, &matWorld)) != D3D_OK) { sprintf(g_tcError,TEXT("Nelze získat transformaci!")); PostQuitMessage(0); } D3DXMatrixRotationY(&matRot, 1*(D3DX_PI/180.0f)); D3DXMatrixMultiply(&matWorld, &matWorld, &matRot); m_lpD3DDevice->SetTransform(D3DTS_WORLD, &matWorld); if((m_lpD3DDevice->BeginScene()) != D3D_OK) { sprintf(g_tcError,TEXT("Nelze začít vykreslovat scénu!")); PostQuitMessage(0); } (m_lpMesh->GetMesh())->DrawSubset(0); if((m_lpD3DDevice->EndScene()) != D3D_OK) { sprintf(g_tcError,TEXT("Nelze ukončit vykreslování scény!")); PostQuitMessage(0); } m_lpD3DDevice->Present(NULL, NULL, NULL, NULL); } } Jako obvykle testujeme na začátku ztrátu zařízení a pokud k ní došlo, tak zařízení obnovujeme. Zde ovšem musíme obnovit i náš mesh objekt, proto ho nejdříve rušíme stejně jako v metodě CWindow::Terminate makrem RELEASE_OBJECT a po obnově zařízení ho opět vytvoříme. Po vymazání obrazovky se provede otočení kolem osy y o jeden stupeň. Samotné renderování mesh objektu provede metoda ID3DXBaseMesh::DrawSubset. Tato metoda má jediný parametr, jímž je zobrazovaná část mesh objektu. Naše věž je zatím mesh objekt jako celek – nemá více částí. Ovšem u složitějších objektů bývá těch částí více a rozdělení se provádí podle přiřazených materiálů (různé části objektu mohou mít přiřazeny různé materiály). O těchto rozděleních se zmíníme v následujících podkapitolách.
4. Direct3D
Prodáno 14.10.2014 na www.Kosmas.cz zákazníkovi [email protected], číslo objednávky 591522, UID: 7e15c933-e4b8-4e2a-94a6-c3fd2dffdec9
DIRECTX
131
V našem programu načítáme pouze jeden objekt. Ve složitějších aplikacích je ale potřeba načíst desítky či stovky takových objektů. Načítání výše uvedeným způsobem by bylo značně neefektivní. V takových případech se vyplatí vytvořit třídu, která bude spravovat všechny instance třídy CMesh, a pro tuto správu je pak vhodné použít datový kontejner vector.
4.9 Materiály Pokud jste předchozí příklad zkusili vykreslovat s vyplněnými plochami, zjistili jste, že je věž celá černá. Vzhledem zobrazovaných ploch jsme se dosud nezabývali. To nyní napravíme, i když i po těchto úpravách jistě nabudete dojmu, že výsledek stále ještě nebude vypadat příliš přívětivě. Důvodem bude skutečnost, že nebudeme zatím pracovat se světly, takže některé materiálové vlastnosti budou zkreslené nebo nemusí být vůbec patrné. Světly se budeme zabývat v následující podkapitole. Nastavení materiálu není příliš komplikované. Základem je struktura D3DMATERIAL9, která je deklarována následovně: typedef struct D3DMATERIAL9 { D3DCOLORVALUE Diffuse; D3DCOLORVALUE Ambient; D3DCOLORVALUE Specular; D3DCOLORVALUE Emissive; float Power; } D3DMATERIAL9, *LPD3DMATERIAL9;
typedef struct D3DCOLORVALUE { float r; float g; float b; float a; } D3DCOLORVALUE, *LPD3DCOLORVALUE; Jednotlivé položky představují barvy v modelu RGB (r – červená, g – zelená, b – modrá) a průhlednost (a – alpha). Hodnoty všech položek struktury D3DCOLORVALUE můžeme zadávat v rozmezí 0–1.
4. Direct3D
Strukturu materiálu tvoří celkem pět položek. První čtyři položky jsou opět struktury (typ D3DCOLORVALUE). Struktura D3DCOLORVALUE má tento tvar:
Když se nyní vrátíme k původní struktuře D3DMATERIAL9, můžeme nastavit barvy difúzní složky, ambientní složky, speculární složky a emisní složky materiálu. Poslední hodnota Power udává ostrost speculární složky. Pokud zatím nedisponujete hlubšími znalostmi z počítačové grafiky a nerozumíte jednotlivým barevným složkám struktury D3DMATERIAL9, stručně se je pokusíme vysvětlit. Nejlepší představu ovšem získáte sami, jakmile s nimi bude v programu experimentovat. Projevy nastavení difúzní, ambientní a speculární složky bez definovaného světla nezaznamenáme. Difúzní složka představuje základní barvu materiálu, která se projeví na plochách objektu, kam dopadá světlo. Speculární složka přestavuje barvu materiálu v místě, kde světla dopadá nejvíce (a přitom se i nejvíce odráží). Naproti tomu barvy ambientní složky se aplikují na všechny plochy, tedy i na ty plochy, které nejsou přímo osvětleny. Pro emisní složku žádné osvětlení nepotřebujeme, protože její barvy způsobují vyzařování této barvy z ploch, kterým je materiál přiřazen.
4.9 Materiály
Prodáno 14.10.2014 na www.Kosmas.cz zákazníkovi [email protected], číslo objednávky 591522, UID: 7e15c933-e4b8-4e2a-94a6-c3fd2dffdec9
132
DIRECTX
Po vytvoření materiálu a naplnění jeho složek se materiál nastavuje metodou SetMaterial, která náleží rozhraní IDirect3DDevice9. Parametrem této metody je ukazatel na strukturu typu D3DMATERIAL9, v níž máme uloženy materiálové vlastnosti. Shrňme si všechny uvedené poznatky do ukázky funkčního programového kódu, který bychom mohli vložit například do metody CreateSources programu z předchozí kapitoly: D3DMATERIAL9 material; ZeroMemory(&material, sizeof(material)); material.Diffuse.r = 0.8; material.Ambient.g = 0.5; material.Emissive.g = 0.1; m_lpD3DDevice->SetMaterial(&material); My ale provedeme v programu větší a efektivnější úpravu. Materiály se totiž v 3D grafických programech exportují do souboru *.x společně s modelem. Proto oproti programu z předchozí kapitoly provedeme takovou úpravu, kdy všechny materiály načteme společně s modelem a nastavíme je. Abychom příklad odlišili od předchozí kapitoly, tentokrát se bude načítaný soubor jmenovat TowerMat.x (na rozdíl od předchozího modelu se liší i obsah souboru – jsou zde nastaveny dva specifické materiály tak, aby byla věž vidět i bez nastavení světel). Jak víme, jméno načítaného souboru máme uloženo v Global.h, symbolické konstantě MESHFILE: #define MESHFILE TEXT("TowerMat.x") V souboru CWindow.cpp nastanou pouze dvě malé úpravy. Jednak vypneme drátové zobrazení (viz předchozí kapitolu) a potom dojde ke změně příkazu vykreslování. Místo příkazu: (m_lpMesh->GetMesh())->DrawSubset(0); vložíme: if(!m_lpMesh->Draw(m_lpD3DDevice)) PostQuitMessage(0); Tedy o vykreslování se bude starat nová členská metoda třídy CMesh s názvem Draw. Všechny ostatní další změny se budou týkat pouze třídy CMesh a jejích metod v příslušných souborech (obsahy ostatních souborů zůstanou nedotčeny). Třída CMesh bude nyní vypadat takto: class CMesh{ private: LPD3DXMESH m_lpD3DXMesh; D3DMATERIAL9 *m_paMaterials; DWORD m_dwNumMat; public: CMesh(void); ~CMesh(void); LPD3DXMESH GetMesh() const {return m_lpD3DXMesh;}
4. Direct3D
Prodáno 14.10.2014 na www.Kosmas.cz zákazníkovi [email protected], číslo objednávky 591522, UID: 7e15c933-e4b8-4e2a-94a6-c3fd2dffdec9
DIRECTX
133
bool Initialize(TCHAR *FileName, LPDIRECT3DDEVICE9 lpDevice); void Terminate(void); bool Draw(LPDIRECT3DDEVICE9 lpDevice); }; Přibyly nám dva soukromé atributy a již zmíněná metoda Draw. První soukromý atribut *m_paMaterials je ukazatel na místo v paměti, kde bude uloženo pole materiálů (instance struktur D3DMATERIAL9). Druhý soukromý atribut je m_dwNumMat a budeme v něm uchovávat počet materiálů a tím pádem i počet částí, které mesh objekt má. Vzhledem k novému ukazateli na pole materiálů došlo k úpravám konstruktoru a metody CMesh::Terminate: CMesh::CMesh(void) { m_lpD3DXMesh = NULL; m_paMaterials = NULL; } void CMesh::Terminate(void) { if(m_paMaterials) { delete [] m_paMaterials; m_paMaterials = NULL; } RELEASE_DX_OBJECT(m_lpD3DXMesh); }
bool CMesh::Initialize(TCHAR *FileName, LPDIRECT3DDEVICE9 lpDevice) { LPD3DXBUFFER lpD3DXMaterialBuffer;
4. Direct3D
Většími změnami prošla metoda CMesh::Create:
if(lpDevice) { if((D3DXLoadMeshFromX(FileName, 0, lpDevice, NULL, &lpD3DXMaterialBuffer, NULL, &m_dwNumMat, & m_lpD3DXMesh)) != D3D_OK) { sprintf(g_tcError,TEXT("Nelze načíst model ze souboru!")); return (false); } m_paMaterials = new D3DMATERIAL9[m_dwNumMat]; D3DXMATERIAL* d3dxMat = (D3DXMATERIAL*)lpD3DXMaterialBuffer->GetBufferPointer();
4.9 Materiály
Prodáno 14.10.2014 na www.Kosmas.cz zákazníkovi [email protected], číslo objednávky 591522, UID: 7e15c933-e4b8-4e2a-94a6-c3fd2dffdec9
134
DIRECTX
for(int i = 0; i < (int)m_dwNumMat; i++) { m_paMaterials[i] = d3dxMat[i].MatD3D; m_paMaterials[i].Ambient = m_paMaterials[i].Diffuse; } return (true); } sprintf(g_tcError,TEXT("Nelze načíst model. Neexistuje objekt Direct3DDevice!")); return (false); } Všimněme si parametrů funkce D3DXLoadMeshFromX. Popsali jsme si je již v předchozí kapitole a uvedli jsme, že materiálů se týká pátý a sedmý parametr. Po úspěšném zavolání této funkce se materiály uloží do bufferu pD3DXMaterialBuffer a počet materiálů do atributu m_dwNumMat. Poté, co známe počet materiálů, již můžeme vytvořit jejich pole příkazem m_paMaterials = new D3DMATERIAL9[m_dwNumMat] a poté v cyklu toto pole naplníme. K naplnění nám pomáhá ukazatel d3dxMat, kde je uložena adresa bufferu s materiály. V cyklu ještě kopírujeme difúzní barvy do ambientních složek. Důvodem je skutečnost, že ambientní složky barev v souboru *.x nejsou uloženy a obvykle bývají stejné jako difúzní barvy. Jako poslední si uveďme tělo metody CMesh::Draw: bool CMesh::Draw(LPDIRECT3DDEVICE9 lpDevice) { if(lpDevice && m_lpD3DXMesh) { for(int i = 0; i < (int)m_dwNumMat; i++) { lpDevice->SetMaterial(&m_paMaterials[i]); m_lpD3DXMesh->DrawSubset(i); } return (true); } sprintf(g_tcError,TEXT("Nelze vykreslit objekt!")); return (false); } V této metodě cyklicky nastavujeme materiály a vykreslujeme všechny části mesh objektu. Připomeňme, že index části vykresleného objektu je ve skutečnosti jediný parametr metody DrawSubset. Pokud jste si nechali vykreslit model věže, který je dodán se zdrojovými kódy této knihy, byly ji přiřazeny dva materiály – střecha bude zelená a spodní část červená. U obou materiálů byly nastaveny emise těchto barevných složek na 0.5.
4.10 Světla Doposud jsme se světlům vyhýbali, nyní to napravíme. Světla patří k nejdůležitějším prvkům scény, protože bez světelných zdrojů bychom toho moc neviděli (výjimkou je nastaveni emitujících barev ve vlastnostech materiálů, jak jsme si ukázali v předchozí pod-
4. Direct3D
Prodáno 14.10.2014 na www.Kosmas.cz zákazníkovi [email protected], číslo objednávky 591522, UID: 7e15c933-e4b8-4e2a-94a6-c3fd2dffdec9
DIRECTX
135
kapitole, ale výsledky nevypadají příliš věrohodně). V této kapitole nejdříve teoreticky popíšeme všechny dostupné světelné zdroje v Direct3D a na závěr jako obvykle některé z těchto světel implementujeme do našeho programu. Světelné zdroje ve scéně nejsou samy o sobě viditelné. Můžeme pouze zaznamenat jejich účinky. Pokud by bylo potřeba nějaký světelný zdroj vidět (například u simulace žárovky), je potřeba vytvořit model světelného zdroje v nějakém grafickém programu a umístit ho do polohy příslušného světelného zdroje. Poté už je jen třeba nastavit, aby paprsky světelného zdroje procházely skrze tento objekt. Než začneme pracovat se světly, musíme mít zapnutou podporu z-bufferu (jak se to dělá, to si ukážeme v příkladu) a také znát normály všech ploch objektů ve scéně. Proč potřebujeme znát normály? Normála je kolmice na každou plochu a na základě normál se počítá, do jaké míry každý ze světelných zdrojů ovlivňuje plochy. Zjednodušeně řečeno se toto pozná podle úhlu, jaký spolu svírá normála plochy a vektor směru šíření světelného paprsku. V praxi to ale nebývá tak jednoduché – pro realističtější vzhled se často používá tzv. Gouraudovo stínování, které vychází z normál ve vertexech, jež vzniknou součtem a normalizací všech normálových vektorů ploch, kterým vertex náleží. Pro lepší názornost je na obrázku 4.11 zobrazena krychle i s normálami ve svých přivrácených plochách a vrcholech. Pokud budeme pracovat s objekty, které si sami vytvoříme v programu, tedy budeme používat vertex buffer, případně i index buffer, je potřeba si ke každému vertexu ukládat normály (nová položka typu D3DVECTOR ve struktuře vertexu). Naštěstí pokud máte modely, jež načítáte z formátu *.x, v tomto souboru jsou normály většinou uloženy (obvykle se dá jejich uložení do souboru volit v grafických programech, ze kterých exportujete modely), takže se o ně nemusíte příliš zajímat. Kromě zapnutí z-bufferu a znalosti normál také musíme aktivovat použití světelných zdrojů při renderování scény. Toto provádí metoda SetRenderState následovně:
Potom již můžeme všechny světlené zdroje nastavit a používat. V Direct3D máme k dispozici celkem čtyři typy světelných zdrojů. Tři z nich se nastavují velice podobným způsobem, poněkud stranou ovšem stojí ambientní světlo. Důvodem je, že pokud zapneme ambientní světlo, tak se nachází v celé scéně o stejné intenzitě a barvě. Neexistuje tedy
4. Direct3D
m_lpD3DDevice->SetRenderState(D3DRS_LIGHTING, TRUE);
Obrázek 4.11: Krychle, normály jejích ploch a normály ve vrcholech
4.10 Světla
Prodáno 14.10.2014 na www.Kosmas.cz zákazníkovi [email protected], číslo objednávky 591522, UID: 7e15c933-e4b8-4e2a-94a6-c3fd2dffdec9
136
DIRECTX
žádná poloha tohoto světelného zdroje. Jeho aktivování se provede následujícím příkazem: m_lpD3DDevice->SetRenderState(D3DRS_AMBIENT, D3DCOLOR_RGBA(127, 127, 127, 0)); Ambientní světlo se aktivuje také metodou SetRenderState, přičemž druhý parametr této funkce představuje barvu světla (v režimu RGBA). Charakteristické pro toto světlo přitom je, že na něj reagují ty materiály, které mají nastaveny ambientní složky. Přitom každá barevná složka ambientního světla ovlivňuje stejnou ambientní barevnou složku materiálu. Pokud tedy chceme mít renderované maximálně červené plochy, nastavíme červenou barvu ambientní složky materiálu na hodnotu 1 a v metodě SetRenderState nastavíme v druhém parametru volání této metody červenou složku barvy na 255. Ostatní typy světelných zdrojů v Direct3D jsou následující:
Bodový všesměrový světelný zdroj – tento zdroj světla je určen barvou a svoji polohou, tedy ho můžeme umístit na libovolnou pozici ve scéně. Z této polohy se paprsky světla rovnoměrně šíří všemi směry se stejnou intenzitou. V níže uvedené struktuře je nutné nastavit položku D3DLIGHTTYPE na D3DLIGHT_POINT. Bodový směrový světelný zdroj (někdy též nazýván světlo typu „spot“) – tento zdroj světla je určen barvou, svoji polohou a směrem, můžeme ho tedy umístit na libovolnou pozici ve scéně a určit směr šíření paprsku. Intenzita světla směrem se vzdáleností od zdroje klesá. Představit si tento světelný zdroj můžeme jako například reflektor. Pokud bychom udělali kolmý řez na směr šíření paprsku, získali bychom dvojitý kruh – šíření světla je dáno dvojicí kuželů se společným vrcholem v místě zdroje světla a osa kuželu představuje směr šíření (viz obrázek 4.12). V níže uvedené struktuře je nutné nastavit položku D3DLIGHTTYPE na D3DLIGHT_SPOT. Plošný směrový světelný zdroj – tento zdroj světla je určen barvou a směrem šíření, tedy nemá pevnou polohu ve scéně. Všechny paprsky jsou vzájemně rovnoběžné a mají stále stejnou intenzitu. Tento světelný zdroj se hodí například k simulaci slunečního světla. V níže uvedené struktuře je nutné nastavit položku D3DLIGHTTYPE na D3DLIGHT_DIRECTIONAL.
Tyto tři typy světelných zdrojů se vytvářejí na základě instance struktury D3DLIGHT9: typedef struct D3DLIGHT9 { D3DLIGHTTYPE Type; D3DCOLORVALUE Diffuse; D3DCOLORVALUE Specular; D3DCOLORVALUE Ambient; D3DVECTOR Position; D3DVECTOR Direction; float Range; float Falloff; float Attenuation0; float Attenuation1; float Attenuation2; float Theta; float Phi; } D3DLIGHT9, *LPD3DLIGHT;
// // // // // // // // // // // // //
typ světelného zdroje barva difúzní složky světla barva speculární složky světla barva ambientní složky světla poloha světelného zdroje směr šíření světla dosah světla pokles intenzity kolmo na směr šíření změna intenzity ve směru od zdroje změna intenzity ve směru od zdroje změna intenzity ve směru od zdroje úhel vnitřního kužele světla úhel vnějšího kužele světla
4. Direct3D
Prodáno 14.10.2014 na www.Kosmas.cz zákazníkovi [email protected], číslo objednávky 591522, UID: 7e15c933-e4b8-4e2a-94a6-c3fd2dffdec9
DIRECTX
137
Komentáře u většiny položek této struktury by měly být pro vysvětlení příslušné položky ve struktuře dostačující. Pro úplnost ještě doplňme některé informace. Range udává dosah světla, tedy pokud se nacházejí plochy mimo tuto vzdálenost od světelného zdroje, nebude mít světlo na tyto plochy žádný účinek. U bodového směrového světelného zdroje jsme si řekli, že směr světla má tvar dvojitých kuželů (viz obrázek 4.12). Zatímco oblast vnitřního kužele je osvětlena plnou intenzitou světla, vnější kužel slouží k tomu, aby se vytvořil přechod mezi osvětlenou a neosvětlenou oblastí (mimo tento kužel již není nic osvětlené). Položka Falloff udává změnu intenzity právě v oblasti vnějšího kužele kolmo na osu kuželů. Častá hodnota této položky je 1 (změny intenzity jsou rovnoměrné). Položky Attenuation0, Attenuation1 a Attenuation2 rovněž definují pokles intenzity, ale ve směru od světelného zdroje až po dosah světla daný položkou Range. Často bývá Attenuation0 nastaveno na hodnotu 1, hodnoty ostatních dvou jsou podle požadovaného úhlu menší (klidně i 0). Poslední dvě položky Theta a Phi představují úhly vnitřního a vnějšího kužele. Obě hodnoty se zadávají v radiánech. Ještě ke struktuře D3DLIGHT9 dodejme, že u různých světelných zdrojů nastavujeme různé položky. Například nemá smysl nastavovat položky Position a Range pro plošný směrový zdroj. Stejně tak nemá smysl nastavovat položky Falloff, Attenuation0, Attenuation1, float Attenuation2, Theta a Phi pro jiný světelný zdroj než bodový směrový. Po naplnění položek instance struktury D3DLIGHT9 je potřeba tento světelný zdroj přiřadit zobrazovacímu zařízení. K tomu se používá metoda Setlight, která má následující tvar: HRESULT IDirect3DDevice9::SetLight(DWORD Index, CONST D3DLight9 * pLight);
Světelný zdroj
4. Direct3D
První parametr představuje index světelného zdroje. Pokud vytváříme první světlo, index bude 0 a potom by měl s každým dalším přidaným světelným zdrojem index narůstat o jedničku. Pokud přiřadíme světelný zdroj se stejným indexem jako světelný zdroj, který již existuje, ten původní se přepíše. Druhý parametr je ukazatel na strukturu D3DLIGHT9, kam jsme doplnili vlastnosti světelného zdroje, které jsme si uvedli výše.
Phi
Theta
Obrázek 4.12: Bodový směrový světelný zdroj
4.10 Světla
Prodáno 14.10.2014 na www.Kosmas.cz zákazníkovi [email protected], číslo objednávky 591522, UID: 7e15c933-e4b8-4e2a-94a6-c3fd2dffdec9
138
DIRECTX
Když hovoříme o indexu světelného zdroje, řekli jsme si, že světel můžeme do scény umístit více. Je to i logické a často se v aplikacích, kde je potřeba, aby se renderovala scéna v podobě blízké realitě, používají alespoň tři světelné zdroje a někdy i více. Pokud vás napadne otázka, jaký je maximální počet světelných zdrojů, které můžete do scény umístit, odpověď je, že pro každé hardwarové vybavení počítače může být tento počet odlišný. Odpověď na tuto otázku dá metoda IDirect3DDevice9::GetDeviceCaps, která je schopna zjistit nejrůznější informace o zobrazovacím zařízení a uložit je do struktury typu D3DCAPS9. Konkrétně maximální počet světel, které můžeme umístit do scény, je v této struktuře uložen v položce MaxActiveLights (typ proměnné je DWORD). Syntaxe metody GetDeviceCaps je následující: HRESULT IDirect3DDevice9::GetDeviceCaps(D3DCAPS9 * pCaps); Po přiřazení světelného zdroje zařízení již můžeme světlo zapnout. To provádí metoda LightEnable, která opět náleží zobrazovacímu zařízení: HRESULT IDirect3DDevice9::LightEnable(DWORD LightIndex, BOOL bEnable); Prvním parametrem je opět index světelného zdroje, s nímž chceme manipulovat. Schválně zde nepíšeme zapínat, protože tato metoda se používá nejen k zapínání, ale umožňuje provádět i opačnou operaci. Pokud je tedy nějaký světelný zdroj zapnutý, touto metodou ho můžeme i vypnout. Zapínání nebo vypínání nám udává druhý parametr metody LightEnable ( 1 - zapíná, 0 – vypíná). Pokud všechny dosažené poznatky shrneme, vytvoření a rozsvícení světla by v našem programu mohlo vypadat například takto: D3DLIGHT9 SpotLight; ZeroMemory(&SpotLight, sizeof(D3DLIGHT9)); SpotLight.Type = D3DLIGHT_SPOT; SpotLight.Diffuse.r = 1.0f; SpotLight.Diffuse.g = 1.0f; SpotLight.Diffuse.b = 1.0f; SpotLight.Position.x = 1.0f; SpotLight.Position.y = 15.0f; SpotLight.Position.z = -1.0f; SpotLight.Direction.x = -1.0f; SpotLight.Direction.y = -15.0f; SpotLight.Direction.z = 1.0f; SpotLight.Range = 50.0f; SpotLight.Falloff = 1.0f; SpotLight.Attenuation0 = 1.0f; SpotLight.Theta = 0.8f; SpotLight.Phi = 1.8f; m_lpD3DDevice->SetLight(0, &SpotLight); m_lpD3DDevice->LightEnable(0, TRUE); Nyní si ukážeme použití světlených zdrojů na konkrétním příkladu. Bude to příklad z předchozí podkapitoly, který opět trochu rozšíříme. Prvním rozšířením bude model. Aby byly účinky světelných zdrojů ve scéně lépe viditelné, nejprve rozšíříme scénu tak, že k věži přibude ještě „země“, která pokryje i okolí věže a na níž bude věž stát. Pro jednoduchost je tato země s věží sjednocena, má ovšem svůj vlastní materiál. Tentokrát bude země zelená, spodní část věže červená a vrch věže modrý. Proti předchozí podkapitole budeme pracovat i se světly, proto tyto barvy nebudou tentokrát nastaveny v parametrech
4. Direct3D
Prodáno 14.10.2014 na www.Kosmas.cz zákazníkovi [email protected], číslo objednávky 591522, UID: 7e15c933-e4b8-4e2a-94a6-c3fd2dffdec9
DIRECTX
139
materiálů Emissive, ale Diffuse. Takto vytvořený model, který je dodán s touto knihou, je uložen v souboru TowerLight.x. Úprava programu proti podkapitole 4.9 nebude složitá. Nejprve v souboru Global.h změníme jméno načítaného modelu #define MESHFILE TEXT("TowerLight.x") Všechny další úpravy se budou týkat pouze metod třídy CWindow. Teoreticky by mohlo stačit zkopírovat vytvoření a aktivace světelného zdroje, jak jsme uvedli výše, na konec metody CWindow::CreateSources. Pokud to takto uděláte a program zkompilujete, po jeho spuštění bude scéna opravdu vykreslena. Stejně to takto můžete udělat i s ostatními typy světelných zdrojů. Ostatní světelné zdroje jsou ve zdrojovém kódu dodaném s touto knihou umístěny v komentáři (pro snadné vyzkoušení jsou všechny příkazy pro vytvoření a zapnutí světla pohromadě a je „porušeno“ pravidlo že deklarace nových datových typů by měly být na začátku každé metody). Jsou ale koncipovány tak, že každé z těchto světel bude svítit samo. Pokud jich chcete aktivovat více najednou, udělat to samozřejmě můžete, ale nezapomeňte změnit indexy světel v metodách SetLight a LightEnable. Dáte-li si takovou scénu renderovat, zjistíte, že vykreslování scény není příliš přesné. Důvodem je, že nemáme aktivovaný z-buffer a také bychom měli povolit i vykreslování ploch, které mají indexy vrcholů umístěné v opačném pořadí. Tento nedostatek se dá vyřešit již známým příkazem: m_lpD3DDevice->SetRenderState(D3DRS_CULLMODE, D3DCULL_NONE); Volání této metody umístíme také do metody CWindow::CreateSources ještě před vytvářením světelných zdrojů. Hned za tento příkaz přidáme také pro jistotu zapnutí všech světel ve scéně: m_lpD3DDevice->SetRenderState(D3DRS_LIGHTING, TRUE);
Dále musíme aktivovat z-buffer. Z-buffer představuje paměť, kde jsou uloženy informace o umístění objektů ve scéně – tedy vzdálenost od polohy kamery. Proto se potom při renderování dá jednoduše zjistit to, co je v popředí a tedy i to, co se má vykreslovat. První kroky pro povolení z-bufferu musíme udělat při vytváření zobrazovacího zařízení. Konkrétně se jedná o nastavení položek EnableAutoDepthStencil a AutoDepthStencilFormat u struktury typu D3DPRESENT_PARAMETERS. V metodě Initialize tedy bude nyní vypadat vytvoření zobrazovacího zařízení takto:
4. Direct3D
Toto je vlastně jen takové pojištění – světla by měla být po spuštění programu standardně povolena, ale je lepší se na toto nespoléhat a příkaz do programu raději vložit.
m_D3Dpp.Windowed = false; m_D3Dpp.SwapEffect = D3DSWAPEFFECT_DISCARD; m_D3Dpp.BackBufferFormat = m_D3DFormat; m_D3Dpp.BackBufferWidth = SCREEN_WIDTH; m_D3Dpp.BackBufferHeight = SCREEN_HEIGHT; m_D3Dpp.hDeviceWindow = m_hWnd; m_D3Dpp.EnableAutoDepthStencil = TRUE; m_D3Dpp.AutoDepthStencilFormat = D3DFMT_D16; // m_D3Dpp.MultiSampleType = D3DMULTISAMPLE_4_SAMPLES; if((m_lpD3D->CreateDevice(0, D3DDEVTYPE_HAL, m_hWnd, D3DCREATE_SOFTWARE_VERTEXPROCESSING, &m_D3Dpp, &m_lpD3DDevice)) != D3D_OK)
4.10 Světla
Prodáno 14.10.2014 na www.Kosmas.cz zákazníkovi [email protected], číslo objednávky 591522, UID: 7e15c933-e4b8-4e2a-94a6-c3fd2dffdec9
140
DIRECTX
{ sprintf(g_tcError,TEXT("Nelze vytvořit objekt Direct3DDevice!")); return (false); } Položka EnableAutoDepthStencil je proměnná typu bool a nastavením této položky na jedničku hloubkový buffer povolíme. Přitom ještě musíme nastavit formát tohoto bufferu (typ D3DFORMAT). Jako častá hodnota se přitom používá D3DFMT_D16, což znamená, že se vytvoří 16bitový z-buffer. Potom ještě v metodě CreateSources tento z-buffer aktivujeme: m_lpD3DDevice->SetRenderState(D3DRS_ZENABLE, TRUE); Poslední věc, kterou musíme udělat, je vymazání z-bufferu při renderování každého nového snímku. To se dá jednoduše provést drobnou úpravou v metodě IDirect3DDevice9::Clear. Ke třetímu parametru, který udává, co se má vymazat, přibude příznak D3DCLEAR_ZBUFFER: m_lpD3DDevice->Clear(0, NULL, D3DCLEAR_TARGET|D3DCLEAR_ZBUFFER, D3DCOLOR_XRGB(255, 255, 255), 1.0, 0);
4.11 Textury V předchozích kapitolách materiály přidaly plochám objektů barvy a světla umožnila tyto barvy zviditelnit. Přesto ale měl vzhled renderované scény do reálné podoby poměrně daleko. Předměty reálného světa obvykle nemají jednotnou barvu, a tak plochy modelů, které se snaží simulovat reálné objekty, nevypadají věrohodně. A právě na tomto místě nastupují textury. Textury jsou vlastně obyčejné obrázky, jež se nanášejí (říká se také, že se „mapují“) na povrchy objektů. S rozvíjející se technikou a možnostmi počítačů se v dnešní době pomalu přibližujeme k zobrazování scén, které mají k realitě skutečně blízko. Zkušení grafici a programátoři grafických aplikací dobře vědí, že si s nanášením textur, které určují pouze barvy na povrchu objektu, již nevystačíme. Textury se dnes používají i k vyvolání dojmu nerovnosti povrchu (tzv. bump mapping) nebo zrcadlových odrazů (tzv. environment mapping). V takovém případě se textury vrství na sebe, přičemž každá vrstva má svůj specifický úkol (jedna ovlivňuje barvy, druhá nerovnosti apod.). Celá problematika vrstvení textur je však příliš složitá, proto se v naší knize spokojíme s aplikováním jednoduchých textur, které budou ovlivňovat pouze barvu ploch. Základní otázka, která vás při nanášení textur jistě napadne, zní: Jak program pozná, která část textury se nanáší na konkrétní místo plochy objektu? Uvedli jsme si, že textury
Obrázek 4.13: Souřadnice textury užité v programech
4. Direct3D
Prodáno 14.10.2014 na www.Kosmas.cz zákazníkovi [email protected], číslo objednávky 591522, UID: 7e15c933-e4b8-4e2a-94a6-c3fd2dffdec9
DIRECTX
141
jsou vlastně obyčejné obrázky, tedy každý pixel v tomto obrázku má svoji pevně danou polohu (souřadnice x a y). Každý obrázek ale může mít jinou velikost. Určitě jste se při hraní nějaké 3D hry setkali s nastavením kvality zobrazování. Obecné pravidlo zní, že čím máte rychlejší a lépe vybavený počítač, tím si můžete spustit hru s více detaily a na objekty se mohou mapovat kvalitnější a tím pádem i z hlediska rozlišení větší textury. Psát programy pro různé počítačové konfigurace by bylo značně neefektivní, proto se při nanášení textur hojně využívá tzv. uv mapování. Při uv mapování má každá textura dvojici souřadnic u a v, které nahrazují klasické souřadnice x, y (viz obrázek 4.16). Tyto souřadnice textury ovšem neodpovídají klasickým souřadnicím. Bez ohledu na rozlišení textury jsou obě tyto souřadnice nastaveny v rozmezí 0–1, což je z obrázku 4.13 rovněž patrné. V programu je pak třeba pomocí těchto uv souřadnic definovat, k jakému vertexu jaká část textury patří. Některé zdroje uvádějí odlišnou orientaci či jiný počátek souřadnic uv na textuře, než jak je uvedeno na obrázku 4.13. Záleží především na koncepci. Obecně vzato ale uv souřadnice mají vždy rozsah 0–1, resp. -1–0. Z výše uvedených informacích je jasné, že chceme-li na objekt mapovat textury, potřebujeme informace o vertexech tohoto objektu rozšířit o uv souřadnice. Struktura vertexu, kterou jsme používali v prvních Direct3D programech, by potom mohla vypadat takto:
// souřadnice polohy vertexu // normálový vektor ve vertexu // uv souřadnice pro texturu
Naštěstí se podobně jako s normálami ploch u složitějších objektů o toto příliš starat nemusíme. Uv souřadnice textury (včetně názvu souboru, ze kterého se textura načítá) mohou být také uloženy v souboru *.x. Úmyslně je zde napsáno „mohou“, protože to, zda jsou tyto souřadnice v souboru uloženy, záleží na grafickém programu, ve kterém byl model vytvořen. V tomto grafickém programu je tak samozřejmě potřeba na model texturu nanést, čili říci, jak se bude mapovat, a poté exportem umožnit uložení uv souřadnic do souboru.
4. Direct3D
struct CUSTOMVERTEX { D3DVECTOR poloha; D3DVECTOR normala; float u, v; };
Možná vás také napadne otázka, jak ale nanést texturu, když se jedná o 2D obrázek, na objekt, který je trojrozměrný? I na to modelovací programy pamatují. Podívejme se na obrázek 4.14. Na něm je vpravo obyčejná krychle, na níž je nanesena textura, takže vypadá jako hrací kostka. Ve skutečnosti na tuto krychli nemusíme nanést šestici obrázků, tedy na každou plochu jeden. Textura, která byla nanesena na tuto krychli, je na obrázku 4.14 vlevo. Textura je sice vždy obdélníková, ale nikdy ji nemusíme na objekt mapovat celou. Vždy se vybere jen ta část obrázku, která je dána uv souřadnicemi jednotlivých vertexů objektu. K texturám dodejme ještě jednu obecnou informaci. Jejich rozměry nemohou být zcela libovolné. Algoritmy, které s texturami pracují, jsou tvořeny s ohledem na to, že rozměry textury budou čtvercové a rozlišení bude vždy mocnina dvou. Takže se používá rozlišení textur 64 × 64, 128 × 128, 256 × 256, 512 × 512 atd. Čísla zde uvedená jsou samozřejmě v pixelech.
4.11 Textury
Prodáno 14.10.2014 na www.Kosmas.cz zákazníkovi [email protected], číslo objednávky 591522, UID: 7e15c933-e4b8-4e2a-94a6-c3fd2dffdec9
142
DIRECTX
Obrázek 4.14: Textura kostky (vlevo), která je nanesena na krychli (vpravo)
Než si ukážeme praktický příklad, ještě si shrňme, co je vlastně v programu třeba udělat. Jako první musíme mít vytvořený objekt rozhraní IDirect3DTexture9, který bude reprezentovat texturu: LPDIRECT3DTEXTURE9 lpD3DTexture; Přes tento objekt budeme moci s texturou pracovat. Samozřejmě ji ale musíme nejdříve načíst. Pokud máme texturu uloženou v nějakém bitmapovém souboru, můžeme využít funkci D3DXCreateTextureFromFile: HRESULT D3DXCreateTextureFromFile( LPDIRECT3DDEVICE9 pDevice, LPCTSTR pSrcFile, LPDIRECT3DTEXTURE9 * ppTexture ); První parametr této funkce je ukazatel na zařízení, ke kterému bude textura asociována. Druhým parametrem této funkce je textový řetězec s názvem souboru (případně i cestou k němu), přičemž je podporováno poměrně velké množství bitmapových formátů – například *.bmp, *.jpg, *.png, *.tga. Třetím parametrem je ukazatel na objekt rozhraní IDirect3DTexture9, které bude reprezentovat načtenou texturu. V případě úspěšného načtení textury funkce D3DXCreateTextureFromFile vrací hodnotu D3D_OK. Potom musíme někde v programu uvést vlastnosti nanášených textur. Těmito vlastnostmi je míněno, že definujeme, co textura vlastně ovlivňuje. Již jsme si řekli, že textura nemusí definovat jenom barvu povrchů objektu, ale například i nerovnost. Pro nastavení stavů slouží metoda IDirect3DDevice9::SetTextureStageState, jejíž obecná syntaxe je následující: HRESULT IDirect3DDevice9::SetTextureStageState( DWORD Stage, D3DTEXTURESTAGESTATETYPE Type, DWORD Value ); První parametr této metody je úroveň textury, tedy vrstva, která se na objekt nanáší. Uvedli jsme si, že se textury mohou vrstvit na sebe a parametr Stage udává pořadí. Začíná se hodnotou 0 a nejvyšší hodnota může být až 7. Celkem tedy na sebe můžeme umístit až osm vrstev. Druhý parametr představuje stav textury, který se má nastavit, a třetí je hod-
4. Direct3D
Prodáno 14.10.2014 na www.Kosmas.cz zákazníkovi [email protected], číslo objednávky 591522, UID: 7e15c933-e4b8-4e2a-94a6-c3fd2dffdec9
DIRECTX
143
nota, jež se přiřazuje tomuto stavu. Pokud nastavení proběhlo v pořádku, jako obvykle se vrací hodnota D3D_OK. Stavů je v Direct3D celkem 19 a každý z nich může mít přiřazeny různé hodnoty, nejrůznějších kombinací nastavení je tedy opravdu hodně. Všechny popisy naleznete v DirectX SDK, takže s nimi můžete experimentovat. My si tu ukážeme nastavení základního stavu, tedy máme jedinou texturu, která bude ovlivňovat pouze barvu: g_pd3dDevice->SetTextureStageState(0, g_pd3dDevice->SetTextureStageState(0, g_pd3dDevice->SetTextureStageState(0, g_pd3dDevice->SetTextureStageState(0,
D3DTSS_COLOROP, D3DTOP_MODULATE); D3DTSS_COLORARG1, D3DTA_TEXTURE); D3DTSS_COLORARG2, D3DTA_DIFFUSE); D3DTSS_ALPHAOP, D3DTOP_DISABLE);
Vidíme, že i pro nastavení jediné textury (první parametr je vždy 0) voláme metodu IDirect3DDevice9::SetTextureStageState celkem čtyřikrát. Zjednodušeně řečeno D3DTSS_COLOROP definuje míchání barev, D3DTSS_COLORARG1 a D3DTSS_COLORARG2 jsou barevné parametry a D3DTSS_ALPHAOP se týká míchání s průhledností. Poslední věc, kterou musíme ještě před vykreslením objektu udělat, je příslušnou texturu aktivovat, aby se na povrch mapovala. To zajistíme zavoláním metody IDirect3DDevice9:: SetTexture. Obecný tvar této metody je následující: HRESULT IDirect3DDevice9::SetTexture( DWORD Sampler, IDirect3DBaseTexture9 * pTexture );
Tolik k teorii. Ukážeme si nanesení textury na praktickém příkladu. Vykreslíme si krychli s texturou hrací kostky tak, jak je uvedeno na obrázku 4.14. Pro tyto účely je u programu dodaného s touto knihou v příslušné složce umístěn soubor modelu krychle i s definovanými normálami a uv souřadnicemi – Cube.x, a příslušná textura – Cube.png. Jako obvykle upravíme program z předchozí podkapitoly. První změna se bude týkat souboru Global.h. Zde změníme název načítaného souboru: #define MESHFILE TEXT("Cube.x")
4. Direct3D
První parametr je úroveň textury, pro kterou ji chceme nastavit, druhý argument je ukazatel na objekt již vytvořené textury. V případě úspěšného nastavení se vrací hodnota D3D_OK.
Žádné další změny v tomto souboru již nebudou. Možná vám to připadá nelogické, protože byste čekali, že zde definujeme i jméno souboru textury, která se bude na krychli nanášet. Skutečnost je ale taková, že jméno souboru textury, která se má na objekt mapovat, již máme uloženo v souboru Cube.x a pokud přidáme šikovnou část algoritmu, můžeme jednoduše toto jméno zjistit, jak uvidíme za chvíli. Pro práci s texturou založíme samostatnou třídu CTexture. Deklarace této třídy a definice jejích metod budou umístěny v souborech CTexture.h a CTexture.cpp. Obsah souboru CTexture.h vypadá následovně: #pragma once class CTexture{ private: LPDIRECT3DTEXTURE9 m_lpD3DTexture; public:
4.11 Textury
Prodáno 14.10.2014 na www.Kosmas.cz zákazníkovi [email protected], číslo objednávky 591522, UID: 7e15c933-e4b8-4e2a-94a6-c3fd2dffdec9
144
DIRECTX
CTexture(void); ~CTexture(void); LPDIRECT3DTEXTURE9 GetTexture(void) const {return m_lpD3DTexture;} bool Initialize(TCHAR *FileName, LPDIRECT3DDEVICE9 lpDevice); void Terminate(void); }; Vystačíme si s jediným atributem, a sice s ukazatelem na objekt rozhraní textury. Kromě standardních metod, které již známe z jiných tříd (jako konstruktor, destruktor a metoda Terminate) obsahuje třída CTexture metodu GetTexture, která pouze vrací ukazatel na objekt, a dále Initialize, která zajistí načtení textury ze souboru a vytvoření objektu textury. Parametrem této metody je jednak jméno souboru, z něhož se má textura načíst, a dále ukazatel na objekt zařízení, pro které se má textura vytvořit. Těla konstruktoru, destruktoru a metody CTexture::Terminate (soubor CTexture.cpp) jsou velice podobná stejným metodám v jiných třídách, protože mají obdobnou funkci. Jsou pouze upravena pro účely této třídy: CTexture::CTexture(void) { m_lpD3DTexture = NULL; } CTexture::~CTexture(void) { Terminate(); } void CTexture::Terminate(void) { RELEASE_DX_OBJECT(m_lpD3DTexture); } Zbývá nám popsat poslední metodu – CTexture::Initialize. Bližší komentář asi není třeba podávat. Jádrem této metody je volání funkce D3DXCreateTextureFromFile, kterou jsme si již popsali. Zbytek této metody je vlastně jen testovací, zda je vše v pořádku – tedy musí existovat zařízení a textura se ze souboru načte. Pokud proběhne vše jak má, metoda CTexture::Initialize vrací hodnotu true, v opačném případě false a v poli znaků g_tcError bude uložen text popisující chybu: bool CTexture::Initialize(TCHAR *FileName, LPDIRECT3DDEVICE9 lpDevice) { if(lpDevice) { if((D3DXCreateTextureFromFile(lpDevice, FileName, &m_lpD3DTexture)) != D3D_OK) { sprintf(g_tcError,TEXT("Nelze načíst texturu ze souboru!")); return (false);
4. Direct3D
Prodáno 14.10.2014 na www.Kosmas.cz zákazníkovi [email protected], číslo objednávky 591522, UID: 7e15c933-e4b8-4e2a-94a6-c3fd2dffdec9
DIRECTX
145
} return(true); } sprintf(g_tcError,TEXT("Nelze načíst texturu. Neexistuje objekt Direct3DDevice!")); return (false); } Nastavení stavu textur provedeme ve třídě CWindow. Konkrétně na konec členské metody CWindow::Initialize přidáme čtveřici známých příkazů: m_lpD3DDevice->SetTextureStageState(0, D3DTOP_MODULATE); m_lpD3DDevice->SetTextureStageState(0, D3DTA_TEXTURE); m_lpD3DDevice->SetTextureStageState(0, D3DTA_DIFFUSE); m_lpD3DDevice->SetTextureStageState(0, D3DTOP_DISABLE);
D3DTSS_COLOROP, D3DTSS_COLORARG1, D3DTSS_COLORARG2, D3DTSS_ALPHAOP,
Ještě zbývá někde v programu vytvořit instanci třídy CTexture a zavolat její metodu pro načtení. Potom je třeba těsně před vykreslením ještě texturu aktivovat. Možná vás napadne, že bychom tuto instanci mohli umístit mezi atributy třídy CWindow. To by samozřejmě šlo. Protože se však ve složitějším programu mohou načítat desítky či stovky textur, nebylo by to programově efektivní a měla by se vytvořit nějaká třída, která bude nějakým způsobem spravovat všechny textury v aplikaci. Kromě toho také platí, že textura (nebo více textur, pokud se vrství na sebe) se v grafických programech přiřazuje jednomu materiálu. Měli bychom proto sloučit skutečnosti, že v místě, kde nastavujeme materiál, nastavíme i texturu.
#include "CTexture.h" Mezi atributy třídy CMesh přidáme ukazatel na pole instancí třídy CTexture:
4. Direct3D
Využijeme toho, že máme pouze jednu texturu, a zbylé úpravy tedy provedeme u třídy CMesh a jejích metod. Do souboru CMesh.h vložíme hned na začátek hlavičkový soubor CTexture.h.
CTexture *m_pTextures; Úměrně tomuto upravíme tělo konstruktoru a metody CMesh::Terminate: CMesh::CMesh(void) { m_lpMesh = NULL; m_paMaterials = NULL; m_pTextures = NULL; } void CMesh::Terminate(void) { if(m_pTextures) { delete [] m_pTextures; m_pTextures = NULL; }
4.11 Textury
Prodáno 14.10.2014 na www.Kosmas.cz zákazníkovi [email protected], číslo objednávky 591522, UID: 7e15c933-e4b8-4e2a-94a6-c3fd2dffdec9
146
DIRECTX
if(m_paMaterials) { delete [] m_paMaterials; m_paMaterials = NULL; } RELEASE_DX_OBJECT(m_lpMesh); } Úprava programu se dotkla i metody CMesh::Initialize (stále jsme ve třídě CMesh), kde načítáme mesh objekt ze souboru *.x i s materiály. Nově přidáme i kód, pomocí něhož načteme i textury: bool CMesh::Initialize(TCHAR *FileName, LPDIRECT3DDEVICE9 lpDevice) { LPD3DXBUFFER lpD3DXMaterialBuffer; if(lpDevice) { if((D3DXLoadMeshFromX(FileName, 0, lpDevice, NULL, &lpD3DXMaterialBuffer, NULL, &m_dwNumMat, &m_lpMesh))!=D3D_OK) { sprintf(g_tcError,TEXT("Nelze načíst model ze souboru!")); return (false); } m_paMaterials = new D3DMATERIAL9[m_dwNumMat]; m_pTextures = new CTexture[m_dwNumMat]; D3DXMATERIAL* d3dxMat = (D3DXMATERIAL*)lpD3DXMaterialBuffer->GetBufferPointer(); for(int i = 0; i < (int)m_dwNumMat; i++) { m_paMaterials[i] = d3dxMat[i].MatD3D; m_paMaterials[i].Ambient = m_paMaterials[i].Diffuse; if(d3dxMat[i].pTextureFilename) { if(!m_pTextures[i].Initialize (d3dxMat[i].pTextureFilename, lpDevice)) return (false); } } return (true); } sprintf(g_tcError,TEXT("Nelze načíst model. Neexistuje objekt Direct3DDevice!")); return (false); }
4. Direct3D
Prodáno 14.10.2014 na www.Kosmas.cz zákazníkovi [email protected], číslo objednávky 591522, UID: 7e15c933-e4b8-4e2a-94a6-c3fd2dffdec9
DIRECTX
147
Jako první si všimněme vytvoření pole textur, kterých bude stejný počet jako materiálů. To zajistíme příkazem m_pTextures = new CTexture[m_dwNumMat];. Dříve jsme si také uvedli, že nepotřebujeme znát jméno souboru textury, protože toto jméno je uloženo v souboru *.x. V našem programu se k tomuto jménu dostaneme položkou d3dxMat[i].pTextureFilename. Pokud by žádné jméno souboru nebylo v této položce uloženo, tak by to znamenalo, že textura neexistuje a nemá smysl nic načítat. Proto je v metodě uvedena podmínka if(d3dxMat[i].pTextureFilename). Poslední metodou, kde došlo k úpravě, je vykreslování – CMesh::Draw: bool CMesh::Draw(LPDIRECT3DDEVICE9 lpDevice) { if(lpDevice && m_lpMesh) { for(int i = 0; i < (int)m_dwNumMat; i++) { lpDevice->SetMaterial(&m_paMaterials[i]); if(m_pTextures[i].GetTexture()) lpDevice->SetTexture(0, m_pTextures[i].GetTexture()); m_lpMesh->DrawSubset(i); } return (true); } sprintf(g_tcError,TEXT("Nelze vykreslit objekt!")); return (false); }
4.12 Další využití textur a transformace objektů Textury mohou mít i širší využití. Když si představíme nějakou moderní 3D aplikaci (například počítačovou hru), ve scéně vidíme najednou desítky či stovky objektů. Pokud si za těmi všemi objekty představíme vertexy, hrany a plochy, dostáváme se do řádů statisíců, někdy i milionů. Ačkoliv jsou počítače stále rychlejší, stále ještě nejsou schopny zvládnout velké množství operací v reálném čase tak, abychom se dostali co nejblíže realitě.
4. Direct3D
Těsně před vykreslením části mesh objektu, jíž odpovídá jeden materiál, se kromě tohoto materiálu aktivuje i textura výše popsanou metodou SetTexture. Podmínkou samozřejmě je, aby tato textura existovala.
Z těchto důvodů se programy různě optimalizují a hledají se cesty, jak ušetřit výpočetní čas počítače. Právě textury mohou za jistých okolností nahradit složité objekty a tím i snížit počet vertexů hran a ploch, které by jinak musel počítač zpracovávat. Jedná se o následující věc. Vytvoříme si čtvercovou či obdélníkovou plochu (tedy dva trojúhelníky) a na ní naneseme texturu nějakého obrázku. V textuře můžeme nastavit jistou barvu, která bude brána jako průhledná, a tím zamezíme obdélníkovému vzhledu. Na obrazovce uvidíme pouze tvar takového objektu. Názorně je to vidět na obrázku 4.15. Vlevo máme strom (texturu) a vpravo je tentýž strom, kde je vidět jeho umístění na ploše dvou trojúhelníků. Kdybychom měli zobrazovat reálný strom, potřebovali bychom velké množství vertexů hran i ploch. Zmíněným způsobem je ušetříme. Taková úprava má ovšem omezené využití. Používá se zejména na vzdálenější objekty (obvykle objekty v pozadí), protože z bližšího pohledu se dá snadno poznat, že
4.12 Další využití textur a transformace...
Prodáno 14.10.2014 na www.Kosmas.cz zákazníkovi [email protected], číslo objednávky 591522, UID: 7e15c933-e4b8-4e2a-94a6-c3fd2dffdec9
148
DIRECTX
Obrázek 4.15: Textura stromu (vlevo) a textura nanesená na plochy dvou trojúhelníků (vpravo)
jde o obrázek, který se umísťuje na 2D plochu. Poznat to jde i u vzdálenějších objektů, protože se tyto plochy s texturami musí neustále natáčet směrem ke kameře, takže vzhled je stále stejný, což u 3D objektů samozřejmě neplatí. U takové „náhrady“ objektů se objevuje ještě jeden problém. Jde o jejich vykreslování. Pokud máme objektů tohoto druhu více, musíme je vykreslovat ve správném pořadí – konkrétně od nejvzdálenějších po nejbližší objekty, aby se správně překrývaly. My si v této kapitole ukážeme implementaci takových textur do našeho programu. V programu musíme provést jen dvě věci. Jednak načíst texturu objektu poněkud odlišným způsobem (musíme definovat, co je v textuře průhledné) a poté vykreslování s průhledností nastavit. Pro načtení takových textur nebudeme nyní používat funkci D3DXCreateTextureFromFile. Místo ní použijeme D3DXCreateTextureFromFileEx, u níž si při nahrávání můžeme určit mnoho věcí, mimo jiné i průhlednost. Obecná syntaxe funkce D3DXCreateTextureFromFileEx je následující: HRESULT D3DXCreateTextureFromFileEx ( LPDIRECT3DDEVICE9 pDevice, LPCTSTR pSrcFile, UINT Width, UINT Height, UINT MipLevels, DWORD Usage, D3DFORMAT Format, D3DPOOL Pool, DWORD Filter, DWORD MipFilter, D3DCOLOR ColorKey, D3DXIMAGE_INFO * pSrcInfo, PALETTEENTRY * pPalette, LPDIRECT3DTEXTURE9 * ppTexture );
4. Direct3D
Prodáno 14.10.2014 na www.Kosmas.cz zákazníkovi [email protected], číslo objednávky 591522, UID: 7e15c933-e4b8-4e2a-94a6-c3fd2dffdec9
DIRECTX
149
Parametrů je celkem čtrnáct. První dva argumenty jsou stejné jako u funkce D3DXCreat eTextureFromFile, tedy ukazatel na zobrazovací zařízení a jméno souboru, případně s cestou k němu. Další dva parametry představují šířku a výšku textury v pixelech. Nejčastěji bývají rozměry textury zachovány tak, jak jsou uloženy v souboru – a v tom případě se tyto dva parametry nastavují na D3DX_DEFAULT. Dále následuje argument MipLevels, představující počet úrovní textury. Vyšší hodnoty udávají více úrovní textury. Úrovně se používají s ohledem na vzdálenost objektu. Různé úrovně textury jsou stejné textury, pouze mají jiné rozlišení. Na vzdálenější objekty se hodí nanášet textury s nižším rozlišením a na bližší objekty zase textury s vyšším rozlišením. Dá se tak docílit reálnějšího vzhledu. Pro více informací můžete nahlédnout do DirectX SDK. My budeme používat jen jednu úroveň (jednu texturu), takže tento parametr budeme nastavovat na hodnotu 1.
Nastavení stavu průhlednosti se provádí také již dříve zmíněnou metodou SetRenderState. Konkrétně tuto metodu musíme zavolat třikrát s různými parametry. Pro zařízení, jehož identifikátor používáme v našich programech, by tedy toto volání vypadalo následovně: m_lpD3DDevice->SetRenderState(D3DRS_ALPHATESTENABLE, TRUE); m_lpD3DDevice->SetRenderState(D3DRS_ALPHAREF, 1); m_lpD3DDevice->SetRenderState(D3DRS_ALPHAFUNC, D3DCMP_GREATEREQUAL);
4. Direct3D
Šestý parametr udává způsob použití textury, pro naše účely ho nebudeme chtít definovat, čili budeme používat hodnotu 0. Dále následuje barevný formát textury specifikovaný podle známého výčtového typu D3DFORMAT. S položkou typu D3DPOOL jsme se již setkali v jiných příkazech. Udává typ paměti, kam se bude textura ukládat. Následuje dvojice příkazů pro filtry a jejich další nastavení. Pokud nechceme mít žádné konkrétní specifikace, do obou parametrů se ukládají hodnoty D3DX_DEFAULT. Potom máme parametr ColorKey, který nás bude nyní nejvíc zajímat. Jde o barevný klíč, který jsme používali již u DirectDraw. Jinými slovy, v tomto parametru se definuje barva, která má být v režimu ARGB, tedy čtyřbytová hodnota – průhlednost, červená, zelená a modrá složka. Pokud má například v textuře černá barva znamenat průhlednou (nejčastější případ), zapsali bychom tento parametr jako D3DCOLOR_ARGB(255,0,0,0). Následuje ukazatel na strukturu typu D3DXIMAGE_INFO, kam se mají uložit informace o načtené textuře. Předposlední parametr je ukazatel na paletu barev (struktura typu PALETTEENTRY), kam by se tato paleta načetla. To by se použilo, pokud by měla textura 256 barev. Protože naše textury používají větší barevnou hloubku, parametr budeme nastavovat jako NULL. Konečně poslední argument představuje ukazatel na objekt rozhraní LPDIRECT3DTEXTURE9, tedy objekt, který bude reprezentovat načtenou texturu.
Tolik teoreticky k průhlednosti. Než ji začleníme do našeho programu z předchozí části, ještě bychom měli vysvětlit druhou část nadpisu této kapitoly – transformace. O transformacích jsme v této knize již hovořili poměrně hodně. Ostatně – objekty, které jsme do scény vkládali, se v posledních programech otáčely kolem osy y. Ale v tom je právě ten problém. Transformace, jež jsme dosud v našem programu aplikovali, se nastavovaly všem objektům ve scéně. To je samozřejmě nežádoucí. Abychom úprav v našem programu provedli více, přidáme metody transformací do třídy CMesh, abychom je mohli aplikovat na každý objekt zvlášť. Vytvoříme si nyní aplikaci, do které implementujeme nově nabyté znalosti. Vyjdeme z programu z předchozí podkapitoly a jako obvykle si popíšeme pouze úpravy. Krychli ale tentokrát vystřídá čtvercová plocha rozdělená na dva trojúhelníky a dále textura stromu, která je na obrázku 4.15. Model i textura jsou v programu dodaném s touto knihou ve dvou verzích. Bez antialiasingu (soubory Tree.x a Tree.png) a s antialiasingem (soubory TreeAA.x a TreeAA.png). Proč tomu tak je? Je to kvůli experimentování s progra-
4.12 Další využití textur a transformace...
Prodáno 14.10.2014 na www.Kosmas.cz zákazníkovi [email protected], číslo objednávky 591522, UID: 7e15c933-e4b8-4e2a-94a6-c3fd2dffdec9
150
DIRECTX
mem. Výše jsme si uvedli, že jako průhledná se dá nastavit jedna definovaná barva, žádné další barvy nebudou průhledné. To může být potíž hlavně u antialiasingu. Vyhlazování se prakticky provádí na rozhraní dvou značně barevně odlišných barev tak, že v okolí rozhraní se barvy rozloží na barevně blízké a vytváří se tak dojem postupného přechodu. Jakmile aplikaci, kterou si tu teď popíšeme, spustíte, neuvidíte čistě strom jako na obrázku 4.15, ale bude obsypán jakoby bílými květy, což jsou právě pixely na rozhraní mezi stromem a průhledným okolím. U modelu s texturou s antialiasingem je těch bílých pixelů znatelně více. To by tak samozřejmě nemělo být (pokud to ovšem není úmysl) a textura by se měla v nějakém grafickém editoru upravit. V souboru Global.h nahradíme stávající definici souboru modelu novou: #define MESHFILE TEXT("Tree.x") Pro načítání textury přidáme do třídy CTexture (soubory CTexture.h a CTexture.cpp) novou metodu pro načítání textur. Ta je definována následovně: bool CTexture::InitializeEx(TCHAR *FileName, LPDIRECT3DDEVICE9 lpDevice, D3DCOLOR argbcolor) { if(lpDevice) { if((D3DXCreateTextureFromFileEx(lpDevice, FileName, D3DX_DEFAULT, D3DX_DEFAULT, D3DX_FROM_FILE, 0, D3DFMT_FROM_FILE, D3DPOOL_DEFAULT, D3DX_DEFAULT, D3DX_DEFAULT, argbcolor, NULL, NULL, &m_lpD3DTexture)) != D3D_OK) { sprintf(g_tcError,TEXT("Nelze načíst texturu ze souboru!")); return (false); } return(true); } sprintf(g_tcError,TEXT("Nelze načíst texturu. Neexistuje objekt Direct3DDevice!")); return (false); } Tělo této metody je velice podobné metodě CTexture::Initialize, kterou jsme již používali v předchozí podkapitole. Jediný rozdíl je, že texturu načítáme voláním funkce D3 DXCreateTextureFromFileEx. Úměrně tomu se změnily parametry metody CTexture:: InitializeEx, se kterými tuto funkci voláme. Pro naše potřeby potřebujeme nastavit průhlednou barvu, proto přibyl třetí parametr D3DCOLOR argbcolor, kde je tato barva specifikována. Samozřejmě bychom tuto metodu mohli dále upravit, kdybychom chtěli při načítání textur specifikovat více parametrů. Protože se textura načítá voláním této metody ze třídy CMesh při otevírání souboru *.x, musíme zde také provést jisté úpravy. Abychom měli vždy možnost volby načítat modely s texturou bez průhlednosti a s ní, přidáme novou členskou metodu InitializeEx. Tělo této metody je zde: bool CMesh::InitializeEx(TCHAR *FileName, LPDIRECT3DDEVICE9 lpDevice, D3DCOLOR argbcolor) {
4. Direct3D
Prodáno 14.10.2014 na www.Kosmas.cz zákazníkovi [email protected], číslo objednávky 591522, UID: 7e15c933-e4b8-4e2a-94a6-c3fd2dffdec9
DIRECTX
151
} Tělo této metody je velice podobné metodě CMesh::Initialize, kterou jsme dosud používali. Rozdíl je ve volání metody pro načtení textur, kdy se tentokrát nevolá metoda CTexture::Initialize, ale její náhrada CTexture::InitializeEx. Proto přibyl třetí parametr metody CMesh::InitializeEx (D3DCOLOR argbcolor), neboť ho musíme předat volané metodě CTexture::InitializeEx.
4. Direct3D
LPD3DXBUFFER lpD3DXMaterialBuffer; if(lpDevice) { if((D3DXLoadMeshFromX(FileName, 0, lpDevice, NULL, &lpD3DXMaterialBuffer, NULL, &m_dwNumMat, & m_lpD3DXMesh)) != D3D_OK) { sprintf(g_tcError,TEXT("Nelze načíst model ze souboru!")); return (false); } m_paMaterials = new D3DMATERIAL9[m_dwNumMat]; m_pTextures = new CTexture[m_dwNumMat]; D3DXMATERIAL* d3dxMat = (D3DXMATERIAL*)lpD3DXMaterialBuffer->GetBufferPointer(); for(int i = 0; i < (int)m_dwNumMat; i++) { m_paMaterials[i] = d3dxMat[i].MatD3D; m_paMaterials[i].Ambient = m_paMaterials[i].Diffuse; if(d3dxMat[i].pTextureFilename) { if(!m_pTextures[i].InitializeEx( d3dxMat[i].pTextureFilename, lpDevice, argbcolor)) return (false); } } return (true); } sprintf(g_tcError,TEXT("Nelze načíst model. Neexistuje objekt Direct3DDevice!")); return (false);
Poslední úpravy provedeme v souboru CWindow.cpp (metody třídy CWindow). Konkrétně aktivujeme průhlednost, přidáme tedy na konec metody CWindow::CreateSources následující trojici příkazů: m_lpD3DDevice->SetRenderState(D3DRS_ALPHATESTENABLE, TRUE); m_lpD3DDevice->SetRenderState(D3DRS_ALPHAREF, 1); m_lpD3DDevice->SetRenderState(D3DRS_ALPHAFUNC, D3DCMP_GREATEREQUAL); Potom ještě musíme v tomto souboru provést změnu při vytváření a načítání objektu ze souboru *.x. V metodě Initialize tedy zaměníme kód: m_lpMesh = new CMesh; if(!m_lpMesh->Initialize(MESHFILE, m_lpD3DDevice)) return (false);
4.12 Další využití textur a transformace...
Prodáno 14.10.2014 na www.Kosmas.cz zákazníkovi [email protected], číslo objednávky 591522, UID: 7e15c933-e4b8-4e2a-94a6-c3fd2dffdec9
152
DIRECTX
kódem novým: m_lpMesh = new CMesh; if(!m_lpMesh->IntializeEx(MESHFILE, m_lpD3DDevice, D3DCOLOR_ARGB(255,255,255,255))) return (false); Z tohoto výpisu je patrné, že u textury stromu je jako průhledná nastavena čistě bílá barva. Tedy všechny tři složky barev RGB jsou na maximální hodnotě 255 (v souboru Tree.png, resp. TreeAA.png je to barva pozadí). Stejnou úpravu musíme samozřejmě provést i v metodě UpdateFrame, kdy při ztrátě zařízení musíme objekt zrušit a opětovně ho zase vytvořit. Pokud bychom to neudělali, po přepnutí do jiné aplikace bychom po návratu zpět viděli strom i s pozadím, protože by nebylo průhledné. Transformace objektu se dají provést poměrně jednoduše. Logicky budeme provádět úpravy ve třídě CMesh a v jejích metodách. Protože si potřebujeme transformace uchovávat, aby se mohly aplikovat při vykreslování, mezi atributy této třídy přidáme transformační matici: D3DXMATRIX *m_lpMatTransf; Tuto matici budeme vytvářet dynamicky, proto ji zde máme jako ukazatel. Její počáteční inicializace proběhne v konstruktoru: m_lpMatTransf = new D3DXMATRIX; D3DXMatrixIdentity(m_lpMatTransf); Funkce D3DXMatrixIdentity vytvoří jednotkovou matici. V metodě CMesh::Terminate se matice ruší: RELEASE_OBJECT(m_lpMatTransf); Další úprava se týká metody vykreslování (metoda CMesh::Draw). Hned na začátek této metody přidáme nastavení transformace objektu podle transformační matice m_lpMatTransf: if(m_lpMatTransf) lpDevice->SetTransform(D3DTS_WORLD, m_lpMatTransf); Potom do třídy CMesh přidáme metody transformací, jež budeme chtít na objekt implementovat. V programu u této knihy jsou implementovány metody pro posun, změnu měřítka a otáčení kolem všech tří os: void CMesh::TranslateXYZ(float TranslX, float TranslY, float TranslZ) { D3DXMATRIX mat; D3DXMatrixTranslation(&mat, TranslX, TranslY, TranslZ); D3DXMatrixMultiply(m_lpMatTransf, m_lpMatTransf, &mat); } void CMesh::ScaleXYZ(float ScaleX, float ScaleY, float ScaleZ) { D3DXMATRIX mat; D3DXMatrixScaling(&mat, ScaleX, ScaleY, ScaleZ); D3DXMatrixMultiply(m_lpMatTransf, m_lpMatTransf, &mat); }
4. Direct3D
Prodáno 14.10.2014 na www.Kosmas.cz zákazníkovi [email protected], číslo objednávky 591522, UID: 7e15c933-e4b8-4e2a-94a6-c3fd2dffdec9
DIRECTX
153
void CMesh::RotateX(float Angle) { D3DXMATRIX mat; D3DXMatrixRotationX(&mat, Angle); D3DXMatrixMultiply(m_lpMatTransf, m_lpMatTransf, &mat); } void CMesh::RotateY(float Angle) { D3DXMATRIX mat; D3DXMatrixRotationY(&mat, Angle); D3DXMatrixMultiply(m_lpMatTransf, m_lpMatTransf, &mat); } void CMesh::RotateZ(float Angle) { D3DXMATRIX mat; D3DXMatrixRotationZ(&mat, Angle); D3DXMatrixMultiply(m_lpMatTransf, m_lpMatTransf, &mat); } Poslední, co musíme udělat, je příslušné transformace v programu zavolat. Konkrétně vypustíme část metody UpdateFrame třídy CWindow, kde jsme otáčeli objektem v prostoru:
D3DXMatrixRotationY(&matRot, 1*(D3DX_PI/180.0f)); D3DXMatrixMultiply(&matWorld, &matWorld, &matRot);
4. Direct3D
if((m_lpD3DDevice->GetTransform(D3DTS_WORLD, &matWorld)) != D3D_OK) { sprintf(g_tcError,TEXT("Nelze získat transformaci!")); PostQuitMessage(0); }
m_lpD3DDevice->SetTransform(D3DTS_WORLD, &matWorld); Místo ní ji nahradíme jediným příkazem, program bude pro jeden objekt ve scéně vlastně dělat to samé: m_lpMesh->RotateY(1*(D3DX_PI/180.0f)); Ještě předtím máme příkaz na mazání obrazovky a z-bufferu. Tentokrát budeme vymazání provádět černou barvou: m_lpD3DDevice->Clear(0, NULL, D3DCLEAR_TARGET|D3DCLEAR_ZBUFFER, D3DCOLOR_XRGB(0, 0, 0), 1, 0); To je vše. Ale protože jsme si do programu přidali i další transformace, můžete zkusit experimentovat s dalšími transformačními metodami. Takto by mohlo například vypadat volání metod pro zvětšování a posunutí objektu: m_lpMesh->ScaleXYZ(1.001f, 1.001f, 1.001f); m_lpMesh->TranslateXYZ(0.001f, 0.001f, 0.001f);
4.12 Další využití textur a transformace...
Prodáno 14.10.2014 na www.Kosmas.cz zákazníkovi [email protected], číslo objednávky 591522, UID: 7e15c933-e4b8-4e2a-94a6-c3fd2dffdec9
154
DIRECTX
4.13 Práce s texty V průběhu výkladu o Direct3D bychom neměli zapomenout popsat práci s texty. Pokud budeme vytvářet aplikace založené na Direct3D, máme celkem čtyři možnosti. První možnost již vlastně známe – jde o klasický text na obrazovce, jaký jsme používali u prvního programu DirectDraw. U Direct3D potřebujeme ukazatel rozhraní surface, do kterého budeme text zapisovat (může to být back buffer, nebo nějaký surface, který jsme si popsali v podkapitole 4.4. Každý surface rozhraní typu IDirect3DSurface9 má členskou metodu GetDC, po jejímž zavolání získáme Handle Device Context tohoto surface (HDC). Potom již pro výpis textu do tohoto surface můžeme použít Win32 API funkce DrawText nebo TextOut. Druhá možnost výpisu obyčejného textu na obrazovku využívá nám dosud neznámé rozhraní ID3DXFont. Jde o rozhraní, jehož objekty představují font. Tento font můžeme vytvořit zavoláním funkce D3DXCreateFont. Jakmile bude příslušný objekt vytvořen, můžeme vypisovat standardní text skrze jeho členskou metodu DrawText. Pokud bychom chtěli vypisovat 3D text, nabízí se další dvě možnosti. První z nich spočívá v tom, že si příslušný text vytvoříme v nějakém 3D grafickém editoru, převedeme ho na klasický mesh objekt a exportujeme ho v grafickém formátu *.x (nebo jiném, který dokáže náš program načíst). Potom se s takovým objektem v programu zachází jako s jiným mesh objektem, s nimiž jsme pracovali v předchozích podkapitolách, tedy můžeme ho zobrazovat i s materiály či texturami. Druhá možnost trojrozměrného textu souvisí s předchozí. Opět se z textu vytvoří mesh objekt. K tomu ale nepotřebujeme 3D grafický program, z něhož bychom exportovali text jako model. Stačí použít funkci D3DXCreateText, kterou zpřístupníme díky vložené knihovně d3dx9.h. Samozřejmě si pak ovšem musíme v programu sami specifikovat materiálové vlastnosti. My si prakticky ukážeme první tři výše zmíněné metody výpisu textu na obrazovku. Než se tak stane, popíšeme si příkazy, které v programu použijeme, ale dosud jsme se s nimi nesetkali. To znamená, že se především zaměříme na způsob výpisu textu prostřednictvím objektu rozhraní ID3DXFont. Jako první musíme deklarovat ukazatel na objekt tohoto rozhraní: LPD3DXFONT m_lpD3DXFont; Potom můžeme tento objekt vytvořit. To se provádí zavoláním funkce D3DXCreateFont, která má celkem dvanáct parametrů: HRESULT D3DXCreateFont( LPDIRECT3DDEVICE9 pDevice, INT Height, UINT Width, UINT Weight, UINT MipLevels, BOOL Italic, DWORD CharSet, DWORD OutputPrecision, DWORD Quality, DWORD PitchAndFamily, LPCTSTR pFacename, LPD3DXFONT * ppFont );
4. Direct3D
Prodáno 14.10.2014 na www.Kosmas.cz zákazníkovi [email protected], číslo objednávky 591522, UID: 7e15c933-e4b8-4e2a-94a6-c3fd2dffdec9
DIRECTX
155
První parametr je ukazatel na rozhraní zařízení IDirect3DDevice9. Dále následuje výška znaků fontu v logických jednotkách (bodech). Třetím argumentem je šířka znaků (opět v logických jednotkách). Parametr Weight představuje váhu fontu, což je vlastně tloušťka písmen. Další argument udává počet úrovní výplní, následuje binární hodnota definující, zda má mít font sklon či nikoliv (Italic). Sedmý parametr definuje použitou znakovou sadu, která musí odpovídat použitému druhu písma. Argument OutputPrecision udává výstupní přesnost vykreslování textu, tedy vztah mezi výškou fontu, šířkou, sklonem a roztečí znaků. Následují kvalita výstupu a rozteč. Předposlední argument je pFacename, což je ukazatel na řetězec obsahující název fontu písma, jenž se má nastavit (například Arial nebo Times). Poslední parametr je ukazatel na objekt rozhraní ID3DXFont, který bude vytvořený font reprezentovat. V případě úspěšného vytvoření objektu se vrací hodnota D3D_OK. Jakmile je font vytvořený, můžeme s jeho pomocí vypisovat text na obrazovku (samozřejmě mezi voláním metod IDirect3DDevice9::BeginScene a IDirect3DDevice9::EndScene). Výpis provádí členská metoda DrawText:
V případě neúspěšného provedení této funkce je návratová hodnota této metody 0. V opačném případě je to výška fontu v logických jednotkách. První parametr metody ID3DXFont:: DrawText je ukazatel na rozhraní typu ID3DXSprite, do něhož se bude text ukládat. Sprite je podobný typ objektu jako textura, tedy mohou se do něj ukládat grafické informace. Pokud žádný sprite není potřeba (což bude i náš případ), tento parametr se nastavuje na hodnotu NULL. Druhý argument funkce představuje vlastní textový řetězec, který se má zobrazit. Třetí parametr je celočíselná hodnota, udávající počet znaků z řetězce, které chceme vypsat od počátku tohoto řetězce. Chceme-li vypsat text celý, nemusíme počítat znaky, ale stačí dosadit na místo tohoto parametru -1. Funkce si počet znaků spočítá sama. Dále následuje instance struktury typu RECT, udávající obdélníkovou oblast, kam se bude text vypisovat. Předposlední parametr specifikuje formát výpisu textu – například zarovnání k obdélníku, který jsme definovali v předchozím argumentu. Konečně poslední argument udává barvu textu i s průhledností.
4. Direct3D
INT ID3DXFont::DrawText( LPD3DXSPRITE pSprite, LPCTSTR pString, INT Count, LPRECT pRect, DWORD Format, D3DCOLOR Color );
Poslední, co ještě potřebujeme, je metoda OnLostDevice: HRESULT ID3DXFont::OnLostDevice(); Ve chvíli, kdy vypíšeme nějaký text za pomoci vytvořeného fontu na obrazovku, vytvoří se vazba na zobrazovací zařízení. Víme, že v případě ztráty musíme toto zařízení obnovit metodou IDirect3DDevice9::Reset. Tato obnova se ale nepodaří, pokud jsou na zařízení nějaké vazby. Metoda OnLostDevice tyto vazby ruší, proto se musí volat v případě ztráty zařízení dříve, než ho obnovíme. Metoda ID3DXFont::OnLostDevice nemá žádné parametry a v případě úspěchu vrací D3D_OK. Nyní již máme dostatek vědomostí, abychom mohli vytvořit funkční program. Vyjdeme opět z programu z předchozí podkapitoly a popíšeme si příslušné úpravy. Jak jsme uvedli výše, vytvoříme program, který bude vypisovat klasické texty přes handle nějakého surface (tento text umístíme v horní části obrazovky), dále skrze objekt rozhraní ID3DXFont
4.13 Práce s texty
Prodáno 14.10.2014 na www.Kosmas.cz zákazníkovi [email protected], číslo objednávky 591522, UID: 7e15c933-e4b8-4e2a-94a6-c3fd2dffdec9
156
DIRECTX
(tento text umístíme do spodní části obrazovky) a dále 3D text jako mesh objekt, který se bude ve scéně otáčet. Ve zdrojovém kódu dodaném s touto knihou je tento mesh objekt uložen v souboru MeshText.x. V souboru Global.h dojde jen k nepatrným změnám. Na začátek vložíme hlavičkový soubor tchar.h pro práci se znakovou sadou UNICODE a provedeme dvě úpravy v symbolických konstantách definující použité soubory: #include #define MESHTEXT TEXT("MeshText.x") #define TEXTIMAGE TEXT("Text.png") Neznámý soubor Text.png slouží jako obrázek načtený do surface, kam se bude ukládat i text. Je to jen obdélníková černá plocha, takže po načtení se příslušný obsah surface vymaže, ale mohl by zde být uložený libovolný obrázek, například úvodní obrazovka nějaké aplikace. Pro práci s rozhraním ID3DXFont vytvoříme novou třídu CFont, kterou deklarujeme v souboru CFont.h a jejíž metody budou definovány v souboru CFont.cpp. Třída CFont bude vypadat takto: #pragma once class CFont{ private: LPD3DXFONT m_lpD3DXFont; RECT m_rcRect; public: CFont(void); ~CFont(void); bool Initialize(LPDIRECT3DDEVICE9 lpDevice, int Height, TCHAR *Facename); void Terminate(void); LPD3DXFONT GetD3DXFont(void) const {return m_lpD3DXFont;} void SetRectD3DXFont(LONG left, LONG top, LONG right, LONG bottom); bool DrawTextD3DXFont(int Count, DWORD Format, D3DCOLOR Color,TCHAR *text,...); bool ReleaseD3DXFont(void); }; V této třídě máme celkem dva atributy. První bude ukazatel na objekt rozraní ID3DXFont a druhý je instance struktury RECT, definující obdélníkovou oblast pro výpis textu. Mezi standardními metodami máme Initialize, která bude objekt fontu vytvářet, SetRectD3DXFont pro nastavení obdélníkové oblasti danou instancí m_rcRect, DrawTextD3DXFont pro výpis textu na obrazovku a ReleaseD3DXFont pro uvolnění vazby objektu fontu na zobrazovací zařízení. Konstruktor, destruktor, metody Terminate a Initialize vypadají následovně: CFont::CFont(void) {
4. Direct3D
Prodáno 14.10.2014 na www.Kosmas.cz zákazníkovi [email protected], číslo objednávky 591522, UID: 7e15c933-e4b8-4e2a-94a6-c3fd2dffdec9
DIRECTX
157
m_lpD3DXFont = NULL; SetRect(&m_rcRect, 0, 0, 0, 0); } CFont::~CFont(void) { Terminate(); } void CFont::Terminate(void) { RELEASE_DX_OBJECT(m_lpD3DXFont); }
Všimněme si parametrů metody CFont::Initialize. Prvním parametrem je objekt zařízení, neboť ho potřebujeme i jako první parametr funkce Initialize. Dále následuje výška písma a jméno fontu. Všechny ostatní parametry nastavujeme s permanentní platností, tedy například střední tloušťka písmen (FW_MEDIUM), text se nebude psát kurzívou (šestý parametr je FALSE) či standardní kvalita výstupu (devátý parametr je DEFAULT_QUALITY). Na všechny možnosti všech parametrů se můžete podívat do dokumentace k funkci Win32 CreateFont, kde je většina parametrů totožných. Tělo metody SetRectD3DXFont je velice jednoduché, protože pouze na základě svých čtyř parametrů nastavuje strukturu m_rcRect:
4. Direct3D
bool CFont::Initialize(LPDIRECT3DDEVICE9 lpD3DDevice, int Height, TCHAR *Facename) { if(D3DXCreateFont(lpD3DDevice, Height, 0, FW_MEDIUM, 1, FALSE, DEFAULT_CHARSET, OUT_DEFAULT_PRECIS, DEFAULT_QUALITY, DEFAULT_PITCH, Facename, &m_lpD3DXFont) != S_OK) { sprintf(g_tcError,TEXT("Nepodařilo se vytvořit font!")); return (false); } return (true); }
void CFont::SetRectD3DXFont(LONG left, LONG top, LONG right, LONG bottom) { SetRect(&m_rcRect, left, top, right, bottom); } Metoda pro výpis textu má zdánlivě tři parametry: bool CFont::DrawTextD3DXFont(int Count, DWORD Format, D3DCOLOR Color, TCHAR *text,...) { TCHAR sz_buff[256]; va_list p_valist; if(m_lpD3DXFont) {
4.13 Práce s texty
Prodáno 14.10.2014 na www.Kosmas.cz zákazníkovi [email protected], číslo objednávky 591522, UID: 7e15c933-e4b8-4e2a-94a6-c3fd2dffdec9
158
DIRECTX
va_start(p_valist, text); _vstprintf(sz_buff, text, p_valist); va_end(p_valist); if(!m_lpD3DXFont->DrawText(NULL, sz_buff, Count, &m_rcRect, Format, Color)) { sprintf(g_tcError,TEXT("Nepodařilo se vypsat text!")); return (false); } else { return (true); } } else { sprintf(g_tcError,TEXT("Neexistuje objekt fontu!")); return (false); } } Avšak těchto parametrů může být ve skutečnosti i více. První parametr je počet znaků textového řetězce, což zde máme pro případ, kdybychom nechtěli vypsat všechny znaky, ale jen určitou část. Potom následuje formátování textu a jeho barva s průhledností. Jako zdánlivě poslední argument je úmyslně ponechán textový řetězec obsahující text, který se má zobrazit. Poslední je proto, že tento textový řetězec nemusí být čistý text, ale může obsahovat například proměnné, které zobrazíme také. Pokud takové proměnné do textu vložíme, budou přenášeny jako další parametry, což symbolizují tři tečky ještě za textovým řetězcem. Případné argumenty musíme převést na standardní text, což provádějí funkce z knihovny tchar.h va_start, _vstprintf a va_end. Výsledkem je tedy již čistý text, který ukládáme do pole znaků sz_buff. Potom se volá metoda DrawText pro výpis textu. Poslední metoda CFont::ReleaseD3DXFont uvolňuje vazbu na zobrazovací zařízení. Z důvodu bezpečnosti obsahuje stejně jako ostatní metody několik podmínek, které zjišťují možné nečekané komplikace: bool CFont::ReleaseD3DXFont(void) { if(m_lpD3DXFont) { if((m_lpD3DXFont->OnLostDevice()) != D3D_OK) { sprintf(g_tcError,TEXT("Nelze uvolnit vazbu fontu na zařízení!")); return (false); } else { return (true); }
4. Direct3D
Prodáno 14.10.2014 na www.Kosmas.cz zákazníkovi [email protected], číslo objednávky 591522, UID: 7e15c933-e4b8-4e2a-94a6-c3fd2dffdec9
DIRECTX
159
} else { sprintf(g_tcError,TEXT("Neexistuje objekt fontu!")); return (false); } } Další úpravy provedeme ve třídě CWindow. Uvedli jsme si, že budeme zobrazovat dohromady tři druhy textů. Celkem v souboru CWindow.h přibudou dva atributy. První je atribut objektu rozhraní surface obrázku m_lpBackImage, do kterého budeme ukládat obyčejný text. Druhým atributem bude dynamická instance třídy CFont s názvem m_lpFont. Pro přehlednost si uvedeme všechny atributy (všechny jsou samozřejmě za návěštím private) třídy CWindow: HWND m_hWnd; LPDIRECT3D9 m_lpD3D; LPDIRECT3DDEVICE9 m_lpD3DDevice; LPDIRECT3DSURFACE9 m_lpBackImage; D3DPRESENT_PARAMETERS m_D3Dpp; D3DFORMAT m_D3DFormat; CMesh *m_lpMesh; CFont *m_lpFont; Kromě těchto atributů přibude nová metoda DrawText2D pro výpis textu do surface: bool DrawText2D(int x, int y, LPDIRECT3DSURFACE9 lpBackImage, TCHAR *text,...);
4. Direct3D
Vzhledem k novým atributům v souboru CWindow.cpp upravíme těla konstruktoru a metody Terminate: CWindow::CWindow(void) { m_hWnd = NULL; m_lpD3D = NULL; m_lpD3DDevice = NULL; m_lpBackImage = NULL; m_lpMesh = NULL; m_lpFont = NULL; ZeroMemory(&m_D3Dpp, sizeof(m_D3Dpp)); m_D3DFormat = (SCREEN_WIDTH == 32) ? D3DFMT_X8R8G8B8 : D3DFMT_R5G6B5; } void CWindow::Terminate(void) { RELEASE_OBJECT(m_lpMesh); RELEASE_OBJECT(m_lpFont);
4.13 Práce s texty
Prodáno 14.10.2014 na www.Kosmas.cz zákazníkovi [email protected], číslo objednávky 591522, UID: 7e15c933-e4b8-4e2a-94a6-c3fd2dffdec9
160
DIRECTX
RELEASE_DX_OBJECT(m_lpBackImage); RELEASE_DX_OBJECT(m_lpD3DDevice); RELEASE_DX_OBJECT(m_lpD3D); CloseWindow(m_hWnd); } Dále na konec metody CWindow::Initialize přidáme následující kód: m_lpMesh = new CMesh; if(!m_lpMesh->Initialize(MESHTEXT, m_lpD3DDevice)) return (false); m_lpFont = new CFont; if(!m_lpFont->Initialize(m_lpD3DDevice, 20, TEXT("Times"))) return (false); m_lpFont->SetRectD3DXFont(0, 459, 0, 0); Nebudeme pracovat s průhledností, proto při vytváření objektu mesh objektu (textu) voláme metodu Initialize a nikoliv InitializeEx. Potom vytváříme dynamickou instanci třídy CFont (velikost písma 20 a klasický font Times New Roman) a specifikujeme oblast, kam se bude text vypisovat. Tento text umístíme do levého spodního rohu obrazovky. Vypisovat se bude od souřadnic 0,459 a další dva mezní parametry pro nás nejsou důležité, proto je máme naplněné hodnotami 0. Na konec metody CWindow::CreateSources přidáme programový kód pro vytvoření surface a načteme do něj obrázek. Jsou to stejné příkazy, které jsme používali v podkapitole 4.4, proto je nebudeme znovu komentovat. Důvodem umístění tohoto kódu do metody CreateSources je to, že musíme tento surface znovu vytvořit, pokud dojde ke ztrátě zařízení. if((m_lpD3DDevice->CreateOffscreenPlainSurface(SCREEN_WIDTH, SCREEN_HEIGHT, m_D3DFormat, D3DPOOL_DEFAULT, &m_lpBackImage, NULL)) != D3D_OK) { sprintf(g_tcError,TEXT("Nelze vytvořit off-screen surface!")); return (false); } if((D3DXLoadSurfaceFromFile(m_lpBackImage, NULL, NULL, TEXTIMAGE, NULL, D3DX_DEFAULT, 0, NULL)) != D3D_OK) { sprintf(g_tcError,TEXT("Nelze načíst obrázek ze souboru!")); return (false); } V metodě CWindow::UpdateFrame musíme v úvodní části při ztrátě zařízení upravit uvolnění všech vazeb na toto zařízení. Po jeho obnovení metodou IDirect3DDevice9::Reset musíme všechny zdroje znovu načíst: if((d3dval=m_lpD3DDevice->TestCooperativeLevel()) != D3D_OK) { if(d3dval == D3DERR_DEVICELOST) return;
4. Direct3D
Prodáno 14.10.2014 na www.Kosmas.cz zákazníkovi [email protected], číslo objednávky 591522, UID: 7e15c933-e4b8-4e2a-94a6-c3fd2dffdec9
DIRECTX
161
if(d3dval == D3DERR_DEVICENOTRESET) { RELEASE_OBJECT(m_lpMesh); RELEASE_DX_OBJECT(m_lpBackImage); if(!(m_lpFont->ReleaseD3DXFont())) PostQuitMessage(0); if((m_lpD3DDevice->Reset(&m_D3Dpp)) != D3D_OK) { sprintf(g_tcError,TEXT("Nelze obnovit zařízení!")); PostQuitMessage(0); return; } Sleep(100); if(!CreateSources()) PostQuitMessage(0); m_lpMesh = new CMesh; if(!m_lpMesh->Initialize(MESHTEXT, m_lpD3DDevice)) PostQuitMessage(0); return; } }
if(m_lpD3DDevice) { m_lpD3DDevice->Clear(0, NULL, D3DCLEAR_TARGET|D3DCLEAR_ZBUFFER, D3DCOLOR_XRGB(0, 0, 0), 1, 0);
4. Direct3D
V další části metody CWindow::UpdateFrame následuje výpis všech textů na obrazovku:
if(!DrawText2D(0, 0, m_lpBackImage, TEXT("Klasický 2D text vypsaný do surface."))) PostQuitMessage(0); m_lpMesh->RotateY(1*(D3DX_PI/180.0f)); if((m_lpD3DDevice->BeginScene()) != D3D_OK) { sprintf(g_tcError,TEXT("Nelze začít vykreslovat scénu!")); PostQuitMessage(0); }
if(!m_lpMesh->Draw(m_lpD3DDevice)) PostQuitMessage(0); if(!m_lpFont->DrawTextD3DXFont(-1, DT_NOCLIP, D3DCOLOR_RGBA(0, 0, 255, 255), TEXT("Toto je text vytvořený
4.13 Práce s texty
Prodáno 14.10.2014 na www.Kosmas.cz zákazníkovi [email protected], číslo objednávky 591522, UID: 7e15c933-e4b8-4e2a-94a6-c3fd2dffdec9
162
DIRECTX
pomocí objektu rozhraní ID3DXFont"))) PostQuitMessage(0); if((m_lpD3DDevice->EndScene()) != D3D_OK) { sprintf(g_tcError,TEXT("Nelze ukončit vykreslování scény!")); PostQuitMessage(0); } m_lpD3DDevice->Present(NULL, NULL, NULL, NULL); } Nejdříve obrazovku vymažeme a voláme metodu DrawText2D pro výpis textu do surface a následné zkopírování tohoto surface do back bufferu (metodu CWindow::DrawText2D si popíšeme níže). Tak vznikne první text v horní části obrazovky. Druhý text, jenž zobrazujeme, je 3D text, který máme jako mesh objekt. Tento mesh se otáčí, proto se před jeho zobrazením volá metoda m_lpMesh->RotateY(1*(D3DX_PI / 180.0f)) – s každou aktualizací obrazovky se objekt otočí o jeden stupeň kolem osy y. Potom se zavoláním metody CMesh::Draw tento mesh objekt zobrazí. Jako poslední se zobrazí text přes objekt fontu m_lpFont. V metodě DrawTextD3DXFont máme druhý parametr DT_NOCLIP (bez odtrhávání) a nastavenou modrou barvu bez průhlednosti. Můžete zkusit poslední hodnotu třetího parametru metody DrawTextD3DXFont snížit – čím nižší bude hodnota, tím bude text průhlednější. Ještě zbývá popsat metodu CWindow::DrawText2D. V programu dodaném s touto knihou ji máme ve dvou verzích. V komentáři je verze, kde se nezapisuje do surface (je to tedy zbytečné), ale přímo do back bufferu. My si ovšem ukážeme verzi se zápisem do surface, teprve potom se tento surface přenáší do back bufferu. bool CWindow::DrawText2D(int x, int y, LPDIRECT3DSURFACE9 lpBackImage, TCHAR *text, ...) { HDC hdc; LPDIRECT3DSURFACE9 lpBackBuffer; TCHAR sz_buff[256]; va_list p_valist; va_start(p_valist, text); _vstprintf(sz_buff, text, p_valist); va_end(p_valist); if(lpBackImage->GetDC(&hdc) == D3D_OK) { SetBkColor(hdc, RGB(255, 255, 255)); SetTextColor(hdc, RGB(0, 255, 0)); SetTextAlign(hdc, TA_LEFT|TA_TOP); TextOut(hdc, x, y, sz_buff, lstrlen(sz_buff)); lpBackImage->ReleaseDC(hdc); } else { sprintf(g_tcError,TEXT("Nelze získat HDC pro výpis textu!")); return (false);
4. Direct3D
Prodáno 14.10.2014 na www.Kosmas.cz zákazníkovi [email protected], číslo objednávky 591522, UID: 7e15c933-e4b8-4e2a-94a6-c3fd2dffdec9
DIRECTX
163
} if((m_lpD3DDevice->GetBackBuffer(0, 0, D3DBACKBUFFER_TYPE_MONO, &lpBackBuffer)) != D3D_OK) { sprintf(g_tcError,TEXT("Nelze získat ukazatel na back buffer!")); return (false); } if((m_lpD3DDevice->StretchRect(lpBackImage, NULL, lpBackBuffer, NULL, D3DTEXF_NONE)) != D3D_OK) { sprintf(g_tcError,TEXT("Nelze zkopírovat obrázek do back bufferu!")); return (false); } RELEASE_DX_OBJECT(lpBackBuffer); return(true); } První dva parametry této metody definují přesnou polohu v surface, kde se bude text vypisovat. Dále následuje ukazatel na objekt surface, do něhož se zápis provede, a nakonec máme vypisovaný text s možnými parametry. V programu potom známým způsobem převádíme případné proměnné v textu na čistý text, který ukládáme do textového pole sz_buff. Potom musíme získat HDC surface (metoda lpBackImage->GetDC(&hdc)).
Funkce TextOut má pět parametrů. První je Handle Device Context zařízení. Dále následuje poloha x a y v surface, kam se text vypíše. Čtvrtým parametrem je pole znaků vypisovaného textu, poslední argument udává počet znaků. Pro výpis celého textu je možné tento počet zjistit funkcí lstrlen(sz_buff). Všechny tyto funkce patří k Win32API, proto pokud chcete podrobnější informace, nahlédněte do příslušné dokumentace.
4. Direct3D
Následují čtyři Win32 funkce – nastavení barvy pozadí (SetBkColor – nastavujeme bílou barvu), nastavení barvy textu (SetTextColor – zelená barva), zarovnání textu (SetTextAlign – nahoru a doleva) a samotný výpis textu (funkce TextOut).
Posléze následuje příkaz pro získání ukazatele na back buffer (metoda GetBackBuffer) a následně přeneseme surface do back bufferu pomocí metody StretchRect. Obě tyto metody jsme popsali v podkapitole 4.4.
4.14 Práce s kamerou a okolí scény Jsme u posledního tématu týkajícího se Direct3D. Tato součást DirectX je nejrozsáhlejší a zasloužila by si jistě i více prostoru, než kolik je jí věnováno v této knize. Pro omezený rozsah však ponecháme na každém čtenáři, jak bude pokračovat dál. Pomoci vám může poslední kapitola této knihy, jejíž náplní jsou doplňující informace a různé tipy. Samozřejmě také doporučuji nahlédnout na seznam zdrojů, které se touto problematikou zabývají – je rovněž uveden na konci knihy. Direct3D uzavřeme komplexnější aplikací. Vytvoříme si plné 3D prostředí, kterým budeme moci procházet, tedy budeme manipulovat s polohou a natočením kamery ve scéně.
4.14 Práce s kamerou a okolí scény
Prodáno 14.10.2014 na www.Kosmas.cz zákazníkovi [email protected], číslo objednávky 591522, UID: 7e15c933-e4b8-4e2a-94a6-c3fd2dffdec9
164
DIRECTX
Do tohoto prostředí implementujeme několik mesh objektů, mimo jiné také tzv. skybox, což je krychle, která vymezuje okolí scény – tvoří pozadí. Také si ukážeme, jak se používá mlha. Protože pro práci s kamerou nepotřebujeme žádné nové Direct3D funkce, které jsme dosud nepoužívali, popis uvedeme až přímo u aplikace. Také skybox by neměl představovat žádný problém, protože půjde o obyčejnou krychli, tedy opět mesh objekt, pouze bude mít velké rozměry a zaměněné normály, aby nesměřovaly vně krychle, ale dovnitř. Na tuto krychli se potom nanáší vhodná textura oblohy. Zbývá tedy podat obecné informace o mlze. Nyní si k ní uvedeme všechny informace a v programu se o ní již nebudeme zmiňovat. Nicméně pokud máte zdrojový kód programu, který je dodán s touto knihou, dva typy mlhy jsou umístěny v souboru CWindow.cpp v komentáři, takže odstraněním příslušného komentáře můžete vybranou mlhu do scény snadno implementovat. Pokud chceme pracovat s mlhou, je třeba pouze provést několik operací. Aktivovat ji, nastavit její barvu, specifikovat typ a doplňující parametry jako je definice oblasti mlhy nebo její hustota. Aktivace mlhy se provádí známou metodou SetRenderState. První parametr této metody specifikujeme jako D3DRS_FOGENABLE a druhý parametr dáme jako TRUE (FALSE by mlhu zakázalo). Tato metoda náleží zařízení, takže v programech z předchozích kapitol by aktivace mlhy vypadala následovně: m_lpD3DDevice->SetRenderState(D3DRS_FOGENABLE, TRUE); Pokud tímto způsobem mlhu aktivujeme, můžeme specifikovat barvu, typ a parametry. Všechny tyto operace se provádí také přes metodu SetRenderState. Barvu bychom určili s prvním parametrem D3DRS_FOGCOLOR, druhým parametrem by byl kód barvy. Bílá mlha by tak mohla být definována jako: m_lpD3DDevice->SetRenderState(D3DRS_FOGCOLOR, D3DXCOLOR(255, 255, 255, 255)); Mlha může být lineární nebo exponenciální. Exponenciální mlha může být dvojího druhu, takže máme celkem tři typy mlhy. Nastavení typu mlhy provádí metoda SetRenderState, která má nastaven první parametr na D3DRS_FOGVERTEXMODE. Druhý parametr se liší podle toho, jakou mlhu chceme. Pro lineární mlhu je tento druhý parametr D3DFOG_LINEAR, pro exponenciální mlhu je D3DFOG_EXP nebo D3DFOG_EXP2. Lineární mlhu vytvoříme takto: m_lpD3DDevice->SetRenderState(D3DRS_FOGVERTEXMODE, D3DFOG_LINEAR); Zbývá nastavit parametry. Specifikaci počátku a konce mlhy provádí metoda SetRenderState, do které dosadíme jako první parametr D3DRS_FOGSTART, případně D3DRS_FOGEND. Druhým parametrem je vzdálenost od kamery. Počátek a konec se udává pouze u lineární mlhy. Definování oblasti by mohlo vypadat takto: float Start = 5.0f; float End = 10.0f; m_lpD3DDevice->SetRenderState(D3DRS_FOGSTART, *(DWORD *)(&Start)); m_lpD3DDevice->SetRenderState(D3DRS_FOGEND, *(DWORD *)(&End)); Pro lineární mlhu se nedefinuje její hustota – ta je určena pouze pro exponenciální typy mlhy. Hustotu definuje metoda SetRenderState s prvním parametrem D3DRS_FOGDENSITY. Druhý parametr je hustota mlhy, takže nastavení hustoty exponenciální mlhy může vypadat například takto: float Density = 0.05f;
4. Direct3D
Prodáno 14.10.2014 na www.Kosmas.cz zákazníkovi [email protected], číslo objednávky 591522, UID: 7e15c933-e4b8-4e2a-94a6-c3fd2dffdec9
DIRECTX
165
m_lpD3DDevice->SetRenderState(D3DRS_FOGDENSITY, *(DWORD *)(&Density)); Tolik k mlze. Nyní popíšeme náš program. Uvedli jsme si, že to bude komplexní scéna s několika objekty. Součástí programu je několik souborů modelů a jim odpovídající textury. Zde je přehled těchto datových souborů s komentáři, které vysvětlují co soubory obsahují. Následující soubory naleznete u zdrojových kódů dodaných s touto knihou.
House.x: model domku.
Chimney.png: textura komína domku – rozlišení 128 × 128 pixelů.
Roof.png: textura střechy domku – rozmíšení 256 × 256 pixelů.
SkyBox.png: textura pozadí scény – rozmíšení 1024 × 1024 pixelů.
SkyBox.x: model pozadí scény.
Terrain.png: textura prostředí – rozlišení 512 × 512 pixelů.
Terrain.x: model prostředí.
TerrainTree.x: model stromu – má pouze materiál, je tedy bez textury.
Wall.png: textura stěny domu – rozlišení 256 × 256 pixelů.
Proti 3D hrám vám bude scéna připadat hodně chudá – z důvodu jednoduchosti je v ní pouze několik málo objektů. Ovládání aplikace se odehrává pomocí kurzorových šipek na klávesnici. Kamerou pohybujeme dopředu nebo dozadu (šipky nahoru a dolů) nebo jí otáčíme do stran (šipky vlevo a vpravo). V horní části obrazovky se vypisují souřadnice polohy kamery. Na obrázku 4-16 je ukázka snímku z této aplikace.
4. Direct3D
Na to, kolik toho aplikace umí, není programového kódu mnoho. Trpí některými neduhy, které by stálo za to vylepšit, ale to už je případná práce pro vás. Pokud se vám nelíbí
Obrázek 4.16: Obrázek z vytvořené 3D aplikace
4.14 Práce s kamerou a okolí scény
Prodáno 14.10.2014 na www.Kosmas.cz zákazníkovi [email protected], číslo objednávky 591522, UID: 7e15c933-e4b8-4e2a-94a6-c3fd2dffdec9
166
DIRECTX
textury, můžete je vylepšit či rovnou vytvořit nové v nějakém kreslicím programu (například Photoshop nebo Gimp). Přitom jim můžete i zvýšit rozlišení, takže mohou být ještě kvalitnější. Další nedostatky aplikace zjistíte jistě sami – například chybí detekce kolizí, takže kamera může procházet skrze objekty. Nemělo by být například složité udělat úpravu změny polohy kamery v souřadnici y tak, aby byla ve stále stejné vzdálenosti od terénu, a tak se vyhnout „vejití“ pod kopec. Můžete si všimnout i světlých pixelů na tmavě modrém pozadí. Byly míněny jako hvězdy na noční obloze. Krychle okolí je ale tak velká, že nestačí ani rozlišení textury 1024 × 1024 pixelů. Nemusíte ovšem toto rozlišení textury nutně zvyšovat. Zmíněný problém by se dal poměrně efektivně vyřešit tak, že krychle nebude statická, ale bude se „pohybovat“ s kamerou tak, aby střed skyboxu byl ve středu kamery. Potom budou mít hvězdy stále stejnou velikost, protože se k nim nikdy nepřiblížíte, a bude to vypadat přirozeněji. A samozřejmě je problém s více zdroji. Pokud budete chtít implementovat do scény více modelů a textur, je vhodné vytvořit nějakou třídu, která se bude o všechny tyto zdroje starat – způsob práce s objekty (vytváření a rušení), jak jsme ho používali v našich programech, není efektivní, a čím více zdrojů bychom tímto způsobem do aplikace přidali, tím by se stala méně přehlednou. Na nějaké další nedostatky jistě přijdete sami a je na vás, budete-li si je chtít opravit a vytvořit nějakou pěknou aplikaci. My se teď až do konce kapitoly budeme věnovat popisu aplikace v té podobě, jak je dodána s touto knihou. Vyjdeme z programů, které jsme vytvářeli dříve, a opět se z důvodu omezeného rozsahu zaměříme pouze na popis novinek. Pro přehlednost si však uveďme všechny soubory i s komentáři, ve kterých je také uvedeno, zda budeme do programových kódů zasahovat:
CApplication.cpp: definice metod třídy pro práci s celou aplikací (žádné úpravy).
CApplication.h: třída pro práci s celou aplikací (žádné úpravy).
CCamera.cpp: definice metod třídy pro práci s kamerou (nový soubor).
CCamera.h: třída pro práci s kamerou (nový soubor).
CFont.cpp: definice metod třídy pro práci s fonty (žádné úpravy).
CFont.h: třída pro práci s fonty (žádné úpravy).
CMesh.cpp: definice metod pro načítání mesh objektů ze souborů *.x (žádné úpravy).
CMesh.h: třída pro načítání mesh objektů ze souborů *.x (žádné úpravy).
CTexture.cpp: definice metod třídy pro práci s texturami (žádné úpravy).
CTexture.h: třída pro práci s texturami (žádné úpravy).
CWindow.cpp: definice metod třídy okna aplikace (větší úpravy).
CWindow.h: třída okna aplikace (malé úpravy).
Global.h: vložené hlavičkové soubory, makra a globální proměnné (malé úpravy).
WinMain.cpp: hlavní smyčka programu s ošetřením smyčky zpráv (malé úpravy).
4. Direct3D
Prodáno 14.10.2014 na www.Kosmas.cz zákazníkovi [email protected], číslo objednávky 591522, UID: 7e15c933-e4b8-4e2a-94a6-c3fd2dffdec9
DIRECTX
167
Začneme souborem Global.h. Oproti programu z předchozí podkapitoly zde máme vloženy symbolické konstanty jiných modelů: #define #define #define #define
MESHTERRAIN TEXT("Terrain.x") MESHTREE TEXT("TerrainTree.x") MESHHOUSE TEXT("House.x") MESHSKYBOX TEXT("SkyBox.x")
Druhou změnou v souboru Global.h je nový globální datový typ pole binárních hodnot: extern bool g_bKeyBuffer[4]; Do tohoto pole budeme ukládat případně stisknuté klávesy. Ty klávesy budeme zjišťovat ve smyčce zpracování zpráv, do tohoto pole je uložíme (v souboru WinMain.cpp) a zjišťovat je budeme v metodách třídy CCamera (soubor CCamera.cpp), kde je toto binární pole rovněž deklarováno. V souboru Global.h je toto pole uvedeno pouze jako extern, abychom se k němu dostali ze smyčky zpráv. Zjišťování stisknutých kláves ve smyčce zpráv v souboru WinMain.cpp vypadá takto:
case WM_KEYDOWN: switch(wParam) { case VK_UP: g_bKeyBuffer[0] = true; break; case VK_DOWN: g_bKeyBuffer[1] = true; break; case VK_LEFT: g_bKeyBuffer[2] = true; break; case VK_RIGHT: g_bKeyBuffer[3] = true; break; case VK_ESCAPE: PostQuitMessage(0); return (0); } break; case WM_DESTROY: PostQuitMessage(0); return (0); }
4. Direct3D
LRESULT CALLBACK WndProc(HWND m_hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam) { switch(uMsg) { case WM_ACTIVATEAPP: b_ActiveApp = wParam; return (0);
4.14 Práce s kamerou a okolí scény
Prodáno 14.10.2014 na www.Kosmas.cz zákazníkovi [email protected], číslo objednávky 591522, UID: 7e15c933-e4b8-4e2a-94a6-c3fd2dffdec9
168
DIRECTX
return DefWindowProc(m_hWnd, uMsg, wParam, lParam); } Stisknuté klávesy se projeví zprávou WM_KEYDOWN. V případě stisknutí kurzorové šipky (hodnota wParam je VK_UP, VK_DOWN, VK_LEFT nebo VK_RIGHT) se příslušná položka v poli nastaví na true. Jiné změny v souboru WinMain.cpp nejsou. Nyní si představíme novou třídu CCamera a její metody. Tato třída má v souboru CCamera.h následující tělo: class CCamera{ private: D3DXVECTOR3 m_vEyePt; D3DXVECTOR3 m_vLookatPt; D3DXVECTOR3 m_vUpVec; D3DXMATRIX m_ViewMatrix; D3DXMATRIX m_ProjectionMatrix; float float float float
m_fOvy; m_fAspect; m_fNearPlane; m_fFarPlane;
public: CCamera(void); ~CCamera(void); bool Initialize(D3DXVECTOR3 vEyePt, D3DXVECTOR3 vLookatPt, D3DXVECTOR3 m_vUpVec, float fovy, float fAspect, float fNearPlane, float fFarPlane, LPDIRECT3DDEVICE9 lpD3DDevice); void Terminate(void); D3DXVECTOR3 GetEyePt(void) const {return(m_vEyePt);} void SetTransformations(LPDIRECT3DDEVICE9 lpD3DDevice); bool SetCamera(LPDIRECT3DDEVICE9 lpD3DDevice); }; Třída CCamera v sobě implementuje několik atributů. V zásadě jsme se již se všemi setkali, protože jsme je používali u pohledových a projekčních transformací. Vektory m_vEyePt, m_vLookatPt a m_vUpVec představují polohu kamery, místo, kam se kamera dívá a směry os pohledu kamery. Pak následují dvě matice: m_ViewMatrix a m_ProjectionMatrix. Prvně jmenovaná je matice pohledu a druhá je projekční matice. Poslední čtyři atributy jsou typu float a jsou to úhel pohledu ve směru osy y (m_fOvy), poměr výšky a šířky obrazu ve scéně (m_fAspect), vzdálenost ořezové roviny, odkud kamera vidí (m_fNearPlane), a vzdálenost ořezové roviny, kam kamera dohlédne (m_fFarPlane). Mezi metodami máme GetEyePt, jež vrací polohu kamery, kterou budeme potřebovat pro výpis souřadnic, kde se nachází, dále Initialize pro naplnění atributů, SetTransformations pro nastavení pohledových a projekčních transformací a SetCamera, kde se bude měnit poloha kamery při stištění příslušných kláves. Metoda Terminate je v našem případě zbytečná (její tělo je prázdné). Jako obvykle je jejím smyslem provádět ukon-
4. Direct3D
Prodáno 14.10.2014 na www.Kosmas.cz zákazníkovi [email protected], číslo objednávky 591522, UID: 7e15c933-e4b8-4e2a-94a6-c3fd2dffdec9
DIRECTX
169
čovací příkazy, které v současné podobě třídy CCamera nejsou potřeba a metoda je zde vložena pouze pro případné další úpravy v budoucnu. V souboru CCamera.cpp najdeme definice všech zmíněných metod. Ještě předtím zde ale máme globální deklaraci binárního pole: bool g_bKeyBuffer[4]; V konstruktoru třídy CCamera pouze toto pole vynulujeme (zpočátku se kamera nebude pohybovat nikam): CCamera::CCamera(void) { g_bKeyBuffer[0] = g_bKeyBuffer[1] = g_bKeyBuffer[2] = g_bKeyBuffer[3] = }
false; false; false; false;
V těle destruktoru pouze voláme metodu Terminate. Výše jsme si uvedli, že její tělo je prázdné – není třeba uvolňovat žádnou paměť, protože nemáme žádné dynamické atributy třídy. Metoda Initialize má celkem osm parametrů. Prvních sedm odpovídá soukromým atributům třídy CCamera a poslední je ukazatel na objekt rozhraní IDirect3DDevice9:
m_fOvy = fOvy; m_fAspect = fAspect; m_fNearPlane = fNearPlane; m_fFarPlane = fFarPlane;
4. Direct3D
bool CCamera::Initialize(D3DXVECTOR3 vEyePt, D3DXVECTOR3 vLookatPt, D3DXVECTOR3 vUpVec, float fOvy, float fAspect, float fNearPlane, float fFarPlane, LPDIRECT3DDEVICE9 lpD3DDevice) { if(lpD3DDevice) { m_vEyePt = vEyePt; m_vLookatPt = vLookatPt; m_vUpVec = vUpVec;
SetTransformations(lpD3DDevice); return (true); } sprintf(g_tcError,TEXT("Neeexistuje objekt Direct3DDevice!")); return (false); } Tento objekt budeme následně potřebovat při nastavení transformací, které provádí metoda SetTransformations, jež se z těla metody Initialize volá. Tělo metody SetTransformations vypadá následovně: void CCamera::SetTransformations(LPDIRECT3DDEVICE9 lpD3DDevice) {
4.14 Práce s kamerou a okolí scény
Prodáno 14.10.2014 na www.Kosmas.cz zákazníkovi [email protected], číslo objednávky 591522, UID: 7e15c933-e4b8-4e2a-94a6-c3fd2dffdec9
170
DIRECTX
D3DXMatrixLookAtLH(&m_ViewMatrix, &m_vEyePt, &m_vLookatPt, &m_vUpVec); lpD3DDevice->SetTransform(D3DTS_VIEW, &m_ViewMatrix); D3DXMatrixPerspectiveFovLH(&m_ProjectionMatrix, m_fOvy, m_fAspect, m_fNearPlane, m_fFarPlane); lpD3DDevice->SetTransform(D3DTS_PROJECTION, &m_ProjectionMatrix); } Uvnitř metody nenalezneme pro nás žádný neznámý programový kód. Nejprve se nastavuje pohledová transformace a poté i projekční. Poslední metodou třídy CCamera je SetCamera. Její tělo je poměrně rozsáhlé: bool CCamera::SetCamera(LPDIRECT3DDEVICE9 lpD3DDevice) { D3DXMATRIX RotMatrix; D3DXVECTOR3 vRotY = D3DXVECTOR3(0.0f, 1.0f, 0.0f); D3DXVECTOR3 vView = m_vLookatPt-m_vEyePt; const float trans = 0.03f; const float rot = 1.0f; const float limitPos = 70.0f; if(!lpD3DDevice) { sprintf(g_tcError,TEXT("Neeexistuje objekt Direct3DDevice!")); return (false); } if(g_bKeyBuffer[0]) { g_bKeyBuffer[0] = false; if((m_vEyePt.x+trans*vView.x > limitPos) || (m_vEyePt.x+trans*vView.x < limitPos) || (m_vEyePt.z+trans*vView.z > limitPos) || (m_vEyePt.z+trans*vView.z < -limitPos)) return (true); m_vEyePt.x += trans*vView.x; m_vLookatPt.x += trans*vView.x; m_vEyePt.z += trans*vView.z; m_vLookatPt.z += trans*vView.z; SetTransformations(lpD3DDevice); } if(g_bKeyBuffer[1]) { g_bKeyBuffer[1] = false; if((m_vEyePt.x-trans*vView.x > limitPos) || (m_vEyePt.x-trans*vView.x < -limitPos) ||
4. Direct3D
Prodáno 14.10.2014 na www.Kosmas.cz zákazníkovi [email protected], číslo objednávky 591522, UID: 7e15c933-e4b8-4e2a-94a6-c3fd2dffdec9
DIRECTX
171
(m_vEyePt.z-trans*vView.z > limitPos) || (m_vEyePt.z-trans*vView.z < -limitPos)) return (true); m_vEyePt.x -= trans*vView.x; m_vLookatPt.x -= trans*vView.x; m_vEyePt.z -= trans*vView.z; m_vLookatPt.z -= trans*vView.z; SetTransformations(lpD3DDevice); } if(g_bKeyBuffer[2]) { D3DXMatrixRotationAxis(&RotMatrix, &vRotY, -rot*(D3DX_PI/180.0f)); D3DXVec3TransformCoord(&vView, &vView, &RotMatrix); m_vLookatPt = m_vEyePt+vView; SetTransformations(lpD3DDevice); g_bKeyBuffer[2] = false; } if(g_bKeyBuffer[3]) { D3DXMatrixRotationAxis(&RotMatrix, &vRotY, rot*(D3DX_PI/180.0f)); D3DXVec3TransformCoord(&vView, &vView, &RotMatrix); m_vLookatPt = m_vEyePt+vView; SetTransformations(lpD3DDevice); g_bKeyBuffer[3] = false; 4. Direct3D
} return (true); } Rozsáhlost je dána tím, že musíme provést všechny potřebné operace při stisknutí některé z kláves. Nejprve deklarujeme lokální datové typy. Jsou to matice, kam budeme ukládat transformaci otáčení kamery RotMatrix, a dva vektory, které zároveň i inicializujeme. Prví je vRotY, což je vektor udávající, že se kamera bude otáčet kolem osy y. Druhý (vView) je vektor směru pohledu kamery. Ten je dán rozdílem vektorů souřadnic místa, kam se kamera dívá, a její polohy. Dále následují tři konstanty. Konstanta reálného čísla trans udává, o kolik se kamera posune, když se stiskne šipka vpřed nebo vzad. Změnou této hodnoty můžete pohyb urychlit nebo zpomalit. Podobný význam má i rot. Tentokrát jde ale o úhel, o který se kamera otočí při stisku šipky vlevo nebo vpravo. Konstanta limitPos vymezuje souřadnice os x a z kamery, kam se až může pohybovat (vertikálně se nepohybujeme, takže souřadnice y jsou stále stejné), a to v kladném i záporném směru. Následuje test, zda objekt zobrazovacího zařízení skutečně existuje. Pokud ne, metoda končí neúspěchem a zbytek těla se neprovede. V případě, že je vše v pořádku, se program může větvit na čtyři možnosti, podle toho, jaká byla stisknutá klávesa. Pokud je
4.14 Práce s kamerou a okolí scény
Prodáno 14.10.2014 na www.Kosmas.cz zákazníkovi [email protected], číslo objednávky 591522, UID: 7e15c933-e4b8-4e2a-94a6-c3fd2dffdec9
172
DIRECTX
obsah g_bKeyBuffer[0] nastaven na true, znamená to, že byla stisknuta šipka vpřed a kamera by se měla posunout ve směru pohledu. To se ale provede pouze tehdy, pokud bychom nepřekročili povolené souřadnice kamery. Podmínka je poměrně rozsáhlá, protože případné nové souřadnice kamery se musí pohybovat v rozmezí (-limitPos, limitPos) v osách x i z. Je-li všechno v pořádku, aktualizuje se nová poloha kamery a samozřejmě i poloha bodu, kam se kamera dívá (tedy směrový vektor je zachován). Velice podobné to je, pokud byla stisknuta šipka dozadu (g_bKeyBuffer[1] je nastaveno na true). Při stisknutí šipek doleva či doprava (g_bKeyBuffer[2] nebo g_bKeyBuffer[3] jsou nastaveny na true) se kamera nepohybuje – pouze se otáčí kolem osy y. V těchto případech se mění pouze cílový bod, kam se kamera dívá. Nejdříve vytváříme rotační matici v ose y funkcí D3DXMatrixRotationAxis. Výsledek je uložen v matici RotMatrix. Potom se volá funkce D3DXVec3TransformCoord, která transformuje vektor (druhý parametr této funkce) a maticí (třetí parametr této funkce). Výsledek se ukládá do prvního parametru této funkce, což je opět ukazatel na vektor. V tomto vektoru budeme mít vypočítanou novou hodnotu vektoru pohledu. Proto stačí nastavit nové souřadnice bodu, kam se díváme příkazem m_vLookatPt=m_vEyePt+vView;. Po všech provedených operacích na konec voláme metodu SetTransformations, čímž se nově vypočítané hodnoty souřadnic polohy kamery a bodu, kam se dívá, uloží a nastaví do transformací. Na základě těchto transformací se aktualizuje vzhled obrazovky. Zbývá popsat obsahy souborů CWindow.h a CWindow.cpp. Jako obvykle, těchto souborů se týká nejvíce změn. Obsah souboru CWindow.h vypadá následovně: #pragma once #include "CCamera.h" #include "CFont.h" #include "CMesh.h" class CWindow { private: HWND m_hWnd; LPDIRECT3D9 m_lpD3D; LPDIRECT3DDEVICE9 m_lpD3DDevice; D3DPRESENT_PARAMETERS m_D3Dpp; D3DFORMAT m_D3DFormat; CCamera *m_lpCamera; CFont *m_lpFont; CMesh *m_lpMeshTerrain, *m_lpMeshTree, *m_lpMeshHouse, *m_lpMeshSkyBox; public: CWindow(void); ~CWindow(void); bool Initialize(void); void Terminate(void); void ReleaseObjects(void); HWND GetHWnd(void) const{return (m_hWnd);}
4. Direct3D
Prodáno 14.10.2014 na www.Kosmas.cz zákazníkovi [email protected], číslo objednávky 591522, UID: 7e15c933-e4b8-4e2a-94a6-c3fd2dffdec9
DIRECTX
173
bool CreateObjects(void); bool CreateSources(void); void UpdateFrame(void); friend LRESULT CALLBACK WndProc(HWND m_hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam); }; Mezi atributy třídy máme ukazatele na nové objekty tříd CCamera, CFont a CMesh. Tím, že jsou to ukazatele, všechny instance těchto tříd budou dynamické. Instance třídy CMesh jsou celkem čtyři – pro každý objekt jeden (krajina, strom, dům a krychle vymezující okolí scény). Také přibyly dvě metody – ReleaseObjects bude všechny objekty rušit a CreateObjects vytvářet. Důvodem těchto metod je zvýšení přehlednosti programu, protože tyto stejné činnosti se provádí při spuštění či ukončení aplikace i při ztrátě zobrazovacího zařízení. Těla těchto dvou metod (to jsme se již dostali k obsahu souboru CWindow.cpp) vypadají takto: bool CWindow::CreateObjects(void) { m_lpMeshTerrain = new CMesh; if(!m_lpMeshTerrain->Create(MESHTERRAIN, m_lpD3DDevice)) return (false); m_lpMeshTree = new CMesh; if(!m_lpMeshTree->Create(MESHTREE, m_lpD3DDevice)) return (false); m_lpMeshTree->ScaleXYZ(5.0f, 5.0f, 5.0f);
m_lpMeshSkyBox = new CMesh; if(!m_lpMeshSkyBox->Create(MESHSKYBOX, m_lpD3DDevice)) return (false);
4. Direct3D
m_lpMeshHouse = new CMesh; if(!m_lpMeshHouse->Create(MESHHOUSE, m_lpD3DDevice)) return (false); m_lpMeshHouse->TranslateXYZ(0.0f, 0.0f, 20.0f); m_lpMeshHouse->ScaleXYZ(1.2f, 1.2f, 1.2f);
return(true); } void CWindow::ReleaseObjects(void) { RELEASE_OBJECT(m_lpMeshTerrain); RELEASE_OBJECT(m_lpMeshTree); RELEASE_OBJECT(m_lpMeshHouse); RELEASE_OBJECT(m_lpMeshSkyBox); } Bližší komentář k těmto metodám snad není potřeba. Dodejme snad jen tolik, že po vytvoření zmíněných objektů na některé z nich aplikujeme transformace v případě, že by nám příslušné vlastnosti, jak jsou uložené v souboru *.x, nevyhovovaly. Například strom je příliš velký a dům je zvětšený a posunutý.
4.14 Práce s kamerou a okolí scény
Prodáno 14.10.2014 na www.Kosmas.cz zákazníkovi [email protected], číslo objednávky 591522, UID: 7e15c933-e4b8-4e2a-94a6-c3fd2dffdec9
174
DIRECTX
Správně bychom ale měli popis metod třídy CWindow začít konstruktorem a destruktorem, respektive metodou Terminate: CWindow::CWindow(void) { m_hWnd = NULL; m_lpD3D = NULL; m_lpD3DDevice = NULL; m_lpCamera = NULL; m_lpFont = NULL; m_lpMeshTerrain = NULL; m_lpMeshTree = NULL; m_lpMeshHouse = NULL; m_lpMeshSkyBox = NULL; ZeroMemory(&m_D3Dpp, sizeof(m_D3Dpp)); m_D3DFormat = (SCREEN_WIDTH == 32) ? D3DFMT_X8R8G8B8 : D3DFMT_R5G6B5; } CWindow::~CWindow(void) { Terminate(); } void CWindow::Terminate(void) { ReleaseObjects(); RELEASE_OBJECT(m_lpCamera); RELEASE_OBJECT(m_lpFont); RELEASE_DX_OBJECT(m_lpD3DDevice); RELEASE_DX_OBJECT(m_lpD3D); CloseWindow(m_hWnd); } V konstruktoru všechny ukazatele budoucích objektů nulujeme. Vytvářet se tyto objekty budou jinde – co se týká grafických objektů, je to záležitostí metody CreateObjects, kterou jsme si již uvedli, instance třídy CCamera a CFont budeme provádět v metodě třídy Initialize. V metodě Terminate se všechny vytvořené instance ruší. Protože se tělo metody Initialize příliš nezměnilo a ke změnám došlo jen na jejím konci, uvedeme si jen tuto část. Zde dochází k vytváření zmíněných instancí tříd CCamera a CFont: m_lpCamera = new CCamera; if(!m_lpCamera->Initialize(D3DXVECTOR3(0.0f, 2.0f, -10.0f), D3DXVECTOR3(0.0f, 1.0f, 0.0f), D3DXVECTOR3(0.0f, 1.0f, 0.0f), D3DX_PI/4, 4.0f/3.0f, 1.0f, 250.0f, m_lpD3DDevice)) return (false); m_lpFont = new CFont;
4. Direct3D
Prodáno 14.10.2014 na www.Kosmas.cz zákazníkovi [email protected], číslo objednávky 591522, UID: 7e15c933-e4b8-4e2a-94a6-c3fd2dffdec9
DIRECTX
175
if(!m_lpFont->CreateD3DXFont(m_lpD3DDevice, 14, TEXT("Times"))) return (false); m_lpFont->SetRectD3DXFont(0, 0, 0, 0); if(!CreateObjects()) return (false); V metodě CreateSources se provádějí standardní příkazy, které jsme zmínili v předchozích podkapitolách. Vymazali jsme odtud ovšem nastavení pohledových a projekčních transformací scény: bool CWindow::CreateSources(void) { D3DXMATRIX matWorld; D3DXMatrixTranslation(&matWorld, 0, 0, 0);
// linearni mlha /* float Start=5.0f; float End=10.0f; m_lpD3DDevice->SetRenderState(D3DRS_FOGENABLE, TRUE); m_lpD3DDevice->SetRenderState(D3DRS_FOGCOLOR, D3DXCOLOR(255, 255, 255, 255)); m_lpD3DDevice->SetRenderState(D3DRS_FOGVERTEXMODE, D3DFOG_LINEAR); m_lpD3DDevice->SetRenderState(D3DRS_FOGSTART, *(DWORD *)(&Start)); m_lpD3DDevice->SetRenderState(D3DRS_FOGEND, *(DWORD *)(&End)); */
4. Direct3D
m_lpD3DDevice->SetTransform(D3DTS_WORLD, &matWorld); m_lpD3DDevice->SetRenderState(D3DRS_ZENABLE, TRUE); m_lpD3DDevice->SetRenderState(D3DRS_CULLMODE, D3DCULL_NONE); m_lpD3DDevice->SetRenderState(D3DRS_LIGHTING, TRUE); m_lpD3DDevice->SetRenderState(D3DRS_AMBIENT, D3DCOLOR_RGBA(255, 255, 255, 0));
// exponencialni mlha /* float Density=0.05f; m_lpD3DDevice->SetRenderState(D3DRS_FOGENABLE, TRUE); m_lpD3DDevice->SetRenderState(D3DRS_FOGCOLOR, D3DXCOLOR(255, 255, 255, 255)); m_lpD3DDevice->SetRenderState(D3DRS_FOGVERTEXMODE, D3DFOG_EXP); m_lpD3DDevice->SetRenderState(D3DRS_FOGDENSITY, *(DWORD *)(&Density)); */ } Pokud byste chtěli aktivovat mlhu, je uvedena v komentáři na konci této metody ve zdrojových souborech dodaných s touto knihou. Poslední metodou třídy CWindow, kterou jsme si ještě nepopsali, je UpdateFrame. Jako obvykle tvoří její první část testování, zda nedošlo ke ztrátě zobrazovacího zařízení – a po-
4.14 Práce s kamerou a okolí scény
Prodáno 14.10.2014 na www.Kosmas.cz zákazníkovi [email protected], číslo objednávky 591522, UID: 7e15c933-e4b8-4e2a-94a6-c3fd2dffdec9
176
DIRECTX
kud ano, tak se provádí příslušná reakce. Tím, že jsme zavedli metody CreateObjects() a ReleaseObjects(), je vše zjednodušeno: if((d3dval=m_lpD3DDevice->TestCooperativeLevel()) != D3D_OK) { if(d3dval == D3DERR_DEVICELOST) return; if(d3dval == D3DERR_DEVICENOTRESET) { ReleaseObjects(); if(!(m_lpFont->ReleaseD3DXFont())) PostQuitMessage(0); if((m_lpD3DDevice->Reset(&m_D3Dpp)) != D3D_OK) { sprintf(g_tcError,TEXT("Nelze obnovit zařízení!")); PostQuitMessage(0); return; } Sleep(100); if(!CreateSources()) PostQuitMessage(0); m_lpCamera->SetTransformations(m_lpD3DDevice); if(!CreateObjects()) PostQuitMessage(0); return; } } Dále je v metodě UpdateFrame provedeno vymazání obrazovky a postupné vykreslení všech objektů: m_lpD3DDevice->Clear(0, NULL, D3DCLEAR_TARGET|D3DCLEAR_ZBUFFER, D3DCOLOR_XRGB(0, 250, 0), 1, 0); if(!m_lpCamera->SetCamera(m_lpD3DDevice)) PostQuitMessage(0); if((m_lpD3DDevice->BeginScene()) != D3D_OK) { sprintf(g_tcError,TEXT("Nelze začít vykreslovat scénu!")); PostQuitMessage(0); } if(!m_lpMeshSkyBox->Draw(m_lpD3DDevice)) PostQuitMessage(0);
4. Direct3D
Prodáno 14.10.2014 na www.Kosmas.cz zákazníkovi [email protected], číslo objednávky 591522, UID: 7e15c933-e4b8-4e2a-94a6-c3fd2dffdec9
DIRECTX
177
if(!m_lpMeshTerrain->Draw(m_lpD3DDevice)) PostQuitMessage(0); m_lpMeshTree->TranslateXYZ(-4.0f, 0.1f, 14.0f); if(!m_lpMeshTree->Draw(m_lpD3DDevice)) PostQuitMessage(0); m_lpMeshTree->TranslateXYZ(4.0f, -0.1f, -14.0f); m_lpMeshTree->RotateY(120*(D3DX_PI / 180.0f)); m_lpMeshTree->TranslateXYZ(6.0f, 0.1f, 16.0f); if(!m_lpMeshTree->Draw(m_lpD3DDevice)) PostQuitMessage(0); m_lpMeshTree->TranslateXYZ(-6.0f, -0.1f, -16.0f); m_lpMeshTree->RotateY(-120*(D3DX_PI/180.0f)); if(!m_lpMeshHouse->Draw(m_lpD3DDevice)) PostQuitMessage(0); vEyePt=m_lpCamera->GetEyePt(); if(!m_lpFont->DrawTextD3DXFont(-1, DT_NOCLIP, D3DCOLOR_RGBA(255, 255, 255, 255), TEXT("Poloha kamery: x = %f, y = %f, z = %f"), vEyePt.x, vEyePt.y, vEyePt.z)) PostQuitMessage(0); if((m_lpD3DDevice->EndScene()) != D3D_OK) { sprintf(g_tcError,TEXT("Nelze ukončit vykreslování scény!")); PostQuitMessage(0); }
Nejprve mažeme obrazovku společně se z-bufferem. V dalším příkazu voláme u objektu m_lpCamera metodu SetCamera, čímž aktualizujeme polohu kamery na obrazovce v případě stisknutí kurzorových kláves a zajistíme správné promítání.
4. Direct3D
m_lpD3DDevice->Present(NULL, NULL, NULL, NULL);
Po začátku renderování začneme postupně zobrazovat všechny objekty ve scéně. Nejprve je to krychle vymezující okolí scény, potom je to krajina. Následuje strom, respektive stromy. Načetli jsme pouze jeden strom, ale díky transformacím ho zobrazujeme dvakrát. Druhý strom je oproti prvnímu posunut a otočen o 120°, čímž docílíme toho, že nebude vypadat stejně. Poté musíme transformace vrátit do původního stavu, aby se v příští aktualizaci stromy zobrazovaly na stejných pozicích. Nakonec se vykresluje model domu. Poslední část výpisu se týká polohy kamery ve scéně. Souřadnice polohy kamery uvádíme jako text. Nejdříve ovšem musíme tuto polohu zjistit, což zajistí zavolání metody CCamera::GetEyePt(). Zjištěné souřadnice se předají jako poslední parametry metody DrawTextD3DXFont. Zavoláním metody objektu zařízení EndScene() se renderování ukončí a obsah back bufferu se v posledním příkazu přenese na obrazovku.
4.14 Práce s kamerou a okolí scény
Prodáno 14.10.2014 na www.Kosmas.cz zákazníkovi [email protected], číslo objednávky 591522, UID: 7e15c933-e4b8-4e2a-94a6-c3fd2dffdec9
178
DIRECTX
4. Direct3D
Prodáno 14.10.2014 na www.Kosmas.cz zákazníkovi [email protected], číslo objednávky 591522, UID: 7e15c933-e4b8-4e2a-94a6-c3fd2dffdec9
DIRECTX
179
5. DirectInput je rozhraní určené pro nejrůznější vstupní periférie. Umožňuje pracovat se zařízeními jako jsou klávesnice, myš či joystick, ale podporuje i různá herní zařízení jako jsou gamepady či volanty. Síla DirectInput spočívá v získávání dat z libovolného vstupního zařízení, bez ohledu na to, o jaké zařízení se jedná. Tato data přitom můžeme získávat i v případě, kdy aplikace běží na pozadí. Služby, které DirectInput poskytuje, byly vytvořeny s ohledem na rychlost a spolehlivost. Díky těmto vlastnostem jde o API, které je vhodné pro interaktivní aplikace, simulace a samozřejmě počítačové hry [14]. Jak se s DirectInput pracuje prakticky si ukážeme ve dvou kapitolách. Než se k těmto příkladům, které jsou v nich uvedeny, dostaneme, popišme si jej alespoň trochu teoreticky.
5. DirectInput
DirectInput
Na začátku každého programu využívajícího DirectInput musíme vložit hlavičkový soubor dinput.h a dále je třeba k programu přilinkovat knihovny dxguid.lib a dinput8.lib. Základem aplikace je vytvořený objekt rozhraní IDirectInput8. Všimněme si osmičky na konci. Ta je tam proto, že poslední rozhraní, které bylo vytvořeno, pochází z DirectX 8. Vzhledem k jeho stabilitě a možnostem nebylo potřeba do tohoto rozhraní zasahovat a používá se i v DirectX9.
5. DirectInput
Prodáno 14.10.2014 na www.Kosmas.cz zákazníkovi [email protected], číslo objednávky 591522, UID: 7e15c933-e4b8-4e2a-94a6-c3fd2dffdec9
180
DIRECTX
Poté, co objekt tohoto rozhraní vytvoříme, je třeba vytvořit objekty rozhraní IDirectInputDevice8. Tyto objekty budou představovat vstupní zařízení, která chceme používat. Předchozí věty byly napsány úmyslně v množném čísle, protože pro každé použité zařízení musíme vytvořit vlastní objekt. Například budeme-li chtít zároveň používat klávesnici i myš, musíme vytvořit jeden objekt zařízení pro klávesnici a druhý pro myš. Následující kroky se potom provádí pro každé vstupní zařízení zvlášť. Dále je třeba stanovit formát dat. Tady definujeme, jak budou vstupní data vypadat. Potom se ještě určí, jak se bude chovat zařízení ke svému okolí, tedy určí se k němu přístupnost z jiných objektů téhož zařízení a okolních aplikací. Nakonec se povolí přístup k datům, která budou posléze k dispozici a aplikace je může získávat. Tento přístup se musí aktivovat pokaždé, když dojde ke ztrátě zařízení. Tedy například při aktivování jiné aplikace klávesovou zkratkou CTRL+TAB a poté při návratu zpět. Na konci programu je třeba uvolnit všechny objekty rozhraní vstupních zařízení IDirectInputDevice8 a posléze i objekt rozhraní IDirectInput8. V následujících dvou podkapitolách se zaměříme zejména na dvojici nejpoužívanějších vstupních zařízení – klávesnici a myš. Pro snadnější výklad obě tato zařízení oddělíme, i když se často používají společně. Pokud však celou problematiku prostudujete dostatečně do hloubky, neměli byste mít problém sloučit práci obou zařízení do jedné třídy. Programy jsou opět tvořeny objektově, tedy vytváříme třídu CInput, která zajišťuje vše potřebné. Tuto knihovnu vkládáme přímo do kódu programu. V praxi se ale často knihovna pro práci se vstupy vytváří jako samostatný projekt, jehož výstupem je knihovna *.dll, která se potom přidává k jednotlivým aplikacím, aby ji tyto aplikace mohly podle potřeby používat.
5.1 Práce s klávesnicí Práci s klávesnicí si popíšeme na jednoduchém programu. Vytvoříme si obrazovku založenou na Direct3D, využijeme také třídu CFont z podkapitoly 4.13 a budeme snímat klávesnici. Na obrazovku pak vypíšeme kódy stisknutých kláves. Projekt se bude skládat z několika souborů, které jsou podobné souborům z kapitol Direct3D. Zde je jejich seznam i s poznámkami o úpravách:
CApplication.cpp: definice metod třídy pro práci s celou aplikací (žádné úpravy).
CApplication.h: třída pro práci s celou aplikací (žádné úpravy).
CFont.cpp: definice metod třídy pro práci s fonty (žádné úpravy).
CFont.h: třída pro práci s fonty (žádné úpravy).
CInput.cp: definice metod třídy pro práci se vstupním zařízením (nový soubor).
CInput.h: třída pro práci se vstupním zařízením (nový soubor).
CWindow.cpp: definice metod třídy okna aplikace (větší úpravy).
CWindow.h: třída okna aplikace (malé úpravy).
Global.h: vložené hlavičkové soubory, makra a globální proměnné (malé úpravy).
WinMain.cpp: hlavní smyčka programu s ošetřením smyčky zpráv (malé úpravy).
5. DirectInput
Prodáno 14.10.2014 na www.Kosmas.cz zákazníkovi [email protected], číslo objednávky 591522, UID: 7e15c933-e4b8-4e2a-94a6-c3fd2dffdec9
DIRECTX
181
Jako obvykle začneme se souborem Global.h. Zde je jeho kompletní výpis: #define _CRT_SECURE_NO_DEPRECATE #define DIRECTINPUT_VERSION
1 0x0800
#pragma once #include #include #include #include #include #include #pragma #pragma #pragma #pragma
<windows.h> <stdio.h> comment comment comment comment
(lib,"dxguid.lib") (lib,"d3d9.lib") (lib,"d3dx9.lib") (lib,"dinput8.lib")
#define APP_NAME TEXT("DirectInput aplikace") #define SCREEN_WIDTH #define SCREEN_HEIGHT #define SCREEN_DEPTH
640 480 32
#define WAITING_FOR_FLIP
10
#define RELEASE_DX_OBJECT(p) #define RELEASE_OBJECT(p)
{if(p){(p)->Release(); p = NULL;};}; {if(p){delete p; p = NULL;};};
extern TCHAR g_tcError[256];
Jak bylo napsáno na začátku kapitoly o DirectInput, potřebujeme do aplikace vložit hlavičkový soubor dinput.h. Knihovnu dxguid.lib jsme přilinkovávali k projektu již dříve. Teď ještě přibývá soubor dinput8.lib. To je vlastně všechno, ostatní makra a pole znaků pro uložení textu, případně vzniklé chyby jsme používali již dříve.
5. DirectInput
Na začátku vkládáme dvě symbolické konstanty. Makro _CRT_SECURE_NO_DEPRECATE známe z kapitol věnovaných DirectDraw a Direct3D. Potlačuje varovné hlášky, které se týkají funkce sprintf v kompilátoru Visual C++ Express Edition. Druhá symbolická konstanta DIRECTINPUT_VERSION potlačuje varovné hlášení kompilátoru. Definuje verzi DirectInput, která se má pro tvorbu objektů použít. Teoreticky bychom mohli toto makro vynechat, protože se vynecháním tohoto makra použije verze 8, tedy to samé, co máme v tomto makru uloženo – pouze potlačíme zmíněná varovná hlášení. Tímto způsobem ale můžeme specifikovat použití i staršího rozhraní – například 7.
V souboru WinMain.cpp došlo k jediné drobné úpravě. Tato úprava se týká smyčky zpracování zpráv. Konkrétně jsme z této smyčky zcela záměrně vypustili reakci na zprávu stisknutí klávesy (WM_KEYDOWN). Zde jsme dříve testovali, jestli nebyla stisknuta klávesa ESCAPE a pokud ano, aplikace se ukončila. Nyní ale budeme vypisovat kódy všech kláves, včetně klávesy ESCAPE. Proto jsme tuto část programu vypustili a celá aplikace se bude ukončovat standardní klávesovou zkratkou ALT+F4.
5.1 Práce s klávesnicí
Prodáno 14.10.2014 na www.Kosmas.cz zákazníkovi [email protected], číslo objednávky 591522, UID: 7e15c933-e4b8-4e2a-94a6-c3fd2dffdec9
182
DIRECTX
Nové jsou po nás v tuto chvíli soubory CInput.h a CInput.cpp. Na začátku souboru CInput.h máme vloženo toto makro: #define KEYDOWN(name, key) (name[key] & 0x80) Jeho smyslem je nalezení hodnoty klávesy key v poli name pro jednotlivé klávesy. Toto makro později použijeme při zjišťování, zda byla příslušná klávesa stisknuta. Zbytek souboru CInput.h tvoří tělo třídy CInput: class CInput{ private: LPDIRECTINPUT8 m_lpDInput; LPDIRECTINPUTDEVICE8 m_lpDIDKeyboard; char m_acKeyboard[256]; public: CInput(void); ~CInput(void); bool Initialize(HINSTANCE hInst, HWND hWnd); void Terminate(void); bool RestoreDevice(void); bool GetKeyboardState(void); bool IsKeyDown(int Key); }; V této třídě máme celkem tři atributy. První je ukazatel na objekt rozhraní IDirectInput8 (m_lpDInput). Dále následuje ukazatel na objekt rozhraní IDirectInputDevice8, které představuje vstupní zařízení (v našem případě to bude klávesnice). Posledním atributem je pole 256 znaků, kam se budou ukládat stavy jednotlivých kláves. Mezi metodami nalezneme čtveřici standardních – konstruktor, destruktor, Initialize (počáteční inicializace) a Terminate (rušení objektů). Dále následuje metoda RestoreDevice, která bude volána pro získání přístupu ke klávesám na začátku po vytvoření potřebných objektů a v případě ztráty zařízení. Předposlední metoda GetKeyboardState bude sloužit ke zjištění stavu všech kláves na klávesnici. Konečně IsKeyDown bude metoda, která zjistí, zda byla stisknuta konkrétní klávesa, která bude jediným vstupním parametrem této funkce. Těla všech metod nalezneme v souboru CInput.cpp. Těla konstruktoru, destruktoru a metody Terminate jsou krátká, proto si je uveďme zároveň: CInput::CInput(void) { m_lpDInput = NULL; m_lpDIDKeyboard = NULL; } CInput::~CInput(void) { Terminate(); }
5. DirectInput
Prodáno 14.10.2014 na www.Kosmas.cz zákazníkovi [email protected], číslo objednávky 591522, UID: 7e15c933-e4b8-4e2a-94a6-c3fd2dffdec9
DIRECTX
183
void CInput::Terminate(void) { RELEASE_DX_OBJECT(m_lpDIDKeyboard); RELEASE_DX_OBJECT(m_lpDInput); } V konstruktoru ještě neexistují objekty rozhraní IDirectInput8 a IDirectInputDevice8, proto se ukazatele na ně naplňují na hodnotu NULL. V metodě Terminate se oba objekty ruší. Oba zmíněné objekty vytváří až metoda Initialize, kterou si popíšeme nyní: bool CInput::Initialize(HINSTANCE hInst, HWND hWnd) { if(DirectInput8Create(hInst, DIRECTINPUT_VERSION, IID_IDirectInput8, (void**)&m_lpDInput, NULL) != DI_OK) { sprintf(g_tcError,TEXT("Nelze vytvořit objekt DirectInput8!")); return (false); } if(m_lpDInput->CreateDevice(GUID_SysKeyboard, &m_lpDIDKeyboard, NULL) != DI_OK) { sprintf(g_tcError,TEXT("Nelze vytvořit objekt klávesnice!")); return (false); }
if(m_lpDIDKeyboard->SetCooperativeLevel(hWnd, DISCL_FOREGROUND|DISCL_NONEXCLUSIVE) != DI_OK) { sprintf(g_tcError,TEXT("Nelze nastavit chování klávesnice!")); return (false); } if(!RestoreDevice()) return (false);
5. DirectInput
if(m_lpDIDKeyboard->SetDataFormat(&c_dfDIKeyboard) != DI_OK) { sprintf(g_tcError,TEXT("Nelze nastavit formát dat pro klávesnici!")); return (false); }
return (true); } Metoda Initialize se volá se dvěma parametry – handle instance aplikace a handle okna. Oboje potřebujeme v příkazech této metody. Všimněte si, že všechny funkce či členské metody, které v těle voláme, testujeme na návratovou hodnotu. Ve všech přípa-
5.1 Práce s klávesnicí
Prodáno 14.10.2014 na www.Kosmas.cz zákazníkovi [email protected], číslo objednávky 591522, UID: 7e15c933-e4b8-4e2a-94a6-c3fd2dffdec9
184
DIRECTX
dech – pokud vše dobře dopadlo – se vrací hodnota DI_OK. Pokud volání některé funkce selže, celá metoda Initialize vrací hodnotu false, v opačném případě true. Nejprve je potřeba zavolat funkci DirectInput8Create. Zde se vytváří objekt rozhraní IDirectInput8. Tato metoda má celkem pět parametrů. První je handle instance aplikace, druhý verze vytvářeného objektu. Zde se často používá symbolická konstanta DIRECTINPUT_VERSION, kterou jsme si definovali v souboru Global.h. Následuje parametr, který udává rozhraní, jež chceme použít. Sem vkládáme IID_IDirectInput8, které je definováno jako IID_IDirectInput8A nebo IID_IDirectInput8W – podle toho, zda je nebo není implementována podpora širokých znaků (UNICODE). Čtvrtým parametrem je adresa ukazatele na objekt rozhraní IDirectInput8, který bude vytvořen. Poslední parametr je ukazatel na adresu, kde se nachází objekt rozhraní IUnknown pro agregaci COM, což využívat nebudeme, takže sem dosazujeme NULL. Po vytvoření objektu DirectInput můžeme vytvářet objekty jednotlivých vstupních zařízení. My budeme nyní pracovat pouze s klávesnicí, proto vytváříme takový objekt pouze jeden. Zmíněnou operaci provádí metoda IDirectInput8::CreateDevice s trojici parametrů. První je identifikátor zařízení, jehož objekt budeme vytvářet. Pro klávesnici se zde vkládá identifikátor GUID_SysKeyboard. Druhým parametrem je adresa ukazatele objektu rozhraní IDirectInputDevice8. Poslední parametr se stejně jako u funkce DirectInput8Create používá pro agregaci COM rozhraní, což nevyužíváme, proto ho nastavíme na NULL. Dále následuje volání dvou metod, tentokrát jde již o metody objektu vstupního zařízení. Nejdříve je to IDirectInputDevice8::SetDataFormat, kde se stanovuje formát dat, získaných ze vstupního zařízení. Tato metoda má jediný parametr, jenž právě tento formát udává. Pro klávesnici je předdefinovaný formát c_dfDIKeyboard, který představuje pole o velikosti 256 bytů (pro každou klávesu je určen jeden z nich). Poté se volá metoda IDirectInputDevice8::SetCooperativeLevel, jež určuje chování vstupního zařízení vůči svému okolí. Má dva parametry. Prvním parametrem je handle okna, ke kterému se má vztahovat, a druhým parametrem může být kombinace příznaků, které toto chování určují. Je celkem pět příznaků, přičemž ne všechny je možné společně zkombinovat, neboť se vzájemně vylučují. DISCL_BACKGROUND udává, že okno aplikace bude mít k zařízení přístup, i když bude na pozadí. DISCL_EXCLUSIVE určuje výhradní přístup k vstupnímu zařízení a DISCL_FOREGROUND specifikuje, že k zařízení je přístup, jen pokud je okno aplikace aktivní. Předposlední příznak DISCL_NONEXCLUSIVE říká, že okno aplikace nebude mít výhradní přístup, takže stejný přístup mohou mít i jiné aplikace. Poslední možný příznak je DISCL_NOWINKEY, který zakáže použití klávesy s logem Windows na klávesnici pro přerušení běhu aplikace. Nakonec metodou CInput::Initialize voláme metodu RestoreDevice, která je také členskou metodou třídy CInput. Zde je její tělo: bool CInput::RestoreDevice(void) { if(!m_lpDInput) { sprintf(g_tcError,TEXT("Neexistuje objekt DirectInput8!")); return (false); } if(m_lpDIDKeyboard->Acquire() != DI_OK)
5. DirectInput
Prodáno 14.10.2014 na www.Kosmas.cz zákazníkovi [email protected], číslo objednávky 591522, UID: 7e15c933-e4b8-4e2a-94a6-c3fd2dffdec9
DIRECTX
185
{ sprintf(g_tcError,TEXT("Nelze nastavit chování klávesnice!")); return (false); } return (true); } O důvodu, proč tento krok provádíme ve zvláštní metodě, jsme se již zmínili. Kromě inicializace musíme metodu IDirectInputDevice8::Acquire zavolat pokaždé, když dojde ke ztrátě zařízení, takže se tento příkaz může za běhu programu volat vícekrát. Metoda Acquire umožňuje přístup k datům příslušného vstupního zařízení, v našem případě tedy klávesnice. Posledními metodami třídy CInput jsou metody GetKeyboardState a IsKeyDown: bool CInput::GetKeyboardState(void) { if(m_lpDIDKeyboard->GetDeviceState(sizeof(m_acKeyboard), &m_acKeyboard) != DI_OK) { sprintf(g_tcError,TEXT("Nelze zjistit stav kláves!")); return (false); } return (true); }
V metodě GetKeyboardState voláme IDirectInputDevice8::GetDeviceState, jež načte data ze vstupního zařízení (tedy klávesnice) a uloží do pole, které je popsáno v parametrech této metody. Jako první se definuje velikost pole, druhým parametrem je adresa tohoto pole. Posléze budou v poli uloženy stavy všech kláves. Ve chvíli, kdy budeme chtít zjistit stav konkrétní klávesy, tedy je-li stisknuta, stačí zavolat metodu CInput::IsKeyDown s kódem této klávesy. Vrací se hodnota true nebo false, podle toho, zda je klávesa stisknuta nebo není. Třída CWindow (soubor CWindow.cpp) vypadá nyní takto:
5. DirectInput
bool CInput::IsKeyDown(int Key) { if(KEYDOWN(m_acKeyboard, Key)) return (true); else return (false); }
class CWindow { private: HWND m_hWnd; LPDIRECT3D9 m_lpD3D; LPDIRECT3DDEVICE9 m_lpD3DDevice; D3DPRESENT_PARAMETERS m_D3Dpp;
5.1 Práce s klávesnicí
Prodáno 14.10.2014 na www.Kosmas.cz zákazníkovi [email protected], číslo objednávky 591522, UID: 7e15c933-e4b8-4e2a-94a6-c3fd2dffdec9
186
DIRECTX
D3DFORMAT m_D3DFormat; CFont *m_lpFont; CInput *m_lpInput; public: CWindow(void); ~CWindow(void); bool Initialize(void); void Terminate(void); HWND GetHWnd(void) const{return (m_hWnd);} bool CreateSources(void); void UpdateFrame(void); friend LRESULT CALLBACK WndProc(HWND m_hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam); }; Aplikace je založena na Direct3D. Protože ale nezobrazujeme na obrazovku žádnou grafiku, pouze kódy stisknutých kláves, je značně zjednodušená a třída CWindow není příliš rozsáhlá. Kromě standardních atributů, které potřebujeme pro provoz Direct3D aplikace, budeme vytvářet objekty dvou tříd – CFont (pro specifikaci písma) a CInput (pro práci s klávesnicí). V konstruktoru třídy CWindow všechny ukazatele inicializujeme na NULL. V metodě Initialize vytváříme čistou Direct3D aplikaci, tedy vytváříme Direct3D objekt a objekt zařízení (nemusíme ani zapínat z-buffer). Potom se z metody Initialize volá metoda CreateSources a ještě se vytváří objekty fontu a vstupního zařízení: m_lpFont = new CFont; if(!m_lpFont->Initialize(m_lpD3DDevice, 14, TEXT("Times"))) return (false); m_lpInput = new CInput; if(!m_lpInput->Initialize(wc.hInstance, m_hWnd)) return (false); V metodě CWindow::CreateSources se pouze nastavují transformace pro vykreslení scény, tedy světové, pohledové a projekční: bool CWindow::CreateSources(void) { D3DXVECTOR3 vEyePt(0.0f, 10.0f, -15.0f); D3DXVECTOR3 vLookatPt(0.0f, 5.0f, 0.0f); D3DXVECTOR3 vUpVec(0.0f, 1.0f, 0.0f); D3DXMATRIX viewMatrix; D3DXMATRIX projectionMatrix; D3DXMATRIX matWorld; D3DXMatrixTranslation(&matWorld, 0, 0, 0); m_lpD3DDevice->SetTransform(D3DTS_WORLD, &matWorld);
5. DirectInput
Prodáno 14.10.2014 na www.Kosmas.cz zákazníkovi [email protected], číslo objednávky 591522, UID: 7e15c933-e4b8-4e2a-94a6-c3fd2dffdec9
DIRECTX
187
D3DXMatrixLookAtLH(&viewMatrix, &vEyePt, &vLookatPt, &vUpVec); m_lpD3DDevice->SetTransform(D3DTS_VIEW, &viewMatrix); D3DXMatrixPerspectiveFovLH(&projectionMatrix, D3DX_PI/4, 4.0f/3.0f, 1.0f, 50.0f); m_lpD3DDevice->SetTransform(D3DTS_PROJECTION, &projectionMatrix); return (true); } Z destruktoru třídy CWindow jako obvykle voláme metodu Terminate, jež ruší všechny atributy této třídy: void CWindow::Terminate(void) { RELEASE_OBJECT(m_lpFont); RELEASE_OBJECT(m_lpInput); RELEASE_DX_OBJECT(m_lpD3DDevice); RELEASE_DX_OBJECT(m_lpD3D); CloseWindow(m_hWnd); } Zajímavější pro nás nyní bude metoda CWindow::UpdateFrame. Zde je úvodní část této metody, která zpracovává případnou ztrátu zařízení: int i; HRESULT d3dval; if((d3dval=m_lpD3DDevice->TestCooperativeLevel()) != D3D_OK) { if(d3dval == D3DERR_DEVICELOST) return;
if((m_lpD3DDevice->Reset(&m_D3Dpp)) != D3D_OK) { sprintf(g_tcError,TEXT("Nelze obnovit zařízení!")); PostQuitMessage(0); return; }
5. DirectInput
if(d3dval == D3DERR_DEVICENOTRESET) { if(!(m_lpFont->ReleaseD3DXFont())) PostQuitMessage(0);
Sleep(100); if(!CreateSources()) PostQuitMessage(0); if(!m_lpInput->RestoreDevice())
5.1 Práce s klávesnicí
Prodáno 14.10.2014 na www.Kosmas.cz zákazníkovi [email protected], číslo objednávky 591522, UID: 7e15c933-e4b8-4e2a-94a6-c3fd2dffdec9
188
DIRECTX
PostQuitMessage(0); return; } } Za povšimnutí stojí volání metody CInput::RestoreDevice v případě, že ke ztrátě zařízení dojde. Obnovení přístupu ke vstupnímu zařízení se provádí až poté, co dojde k obnově zařízení pro zobrazení. Ve zbylé části metody CWindow::UpdateFrame vymažeme obrazovku a zjistíme aktuální stav klávesnice. Potom již můžeme zjišťovat stavy jednotlivých kláves (na obrazovku se nahoře uprostřed vypíše text „Stisknuté klávesy:“ a potom se podle toho, jaké klávesy budou stisknuté, zobrazí jejich kód. Díky DirectInput je možné detekovat i více kláves, které jsou stisknuty současně. m_lpD3DDevice->Clear(0, NULL, D3DCLEAR_TARGET, D3DCOLOR_XRGB(0, 0, 0), 1, 0); if(!m_lpInput->GetKeyboardState()) PostQuitMessage(0); if((m_lpD3DDevice->BeginScene()) != D3D_OK) { sprintf(g_tcError,TEXT("Nelze začít vykreslovat scénu!")); PostQuitMessage(0); } m_lpFont->SetRectD3DXFont(300, 0, 0, 0); if(!m_lpFont->DrawTextD3DXFont(-1, DT_NOCLIP, D3DCOLOR_RGBA(0, 255, 255, 255), TEXT("Stisknuté klávesy:"))) PostQuitMessage(0); for(i = 0; i < 51; i++ ) { if(m_lpInput->IsKeyDown(i)) { m_lpFont->SetRectD3DXFont(0, 9*i, 0, 0); if(!m_lpFont->DrawTextD3DXFont(-1, DT_NOCLIP, D3DCOLOR_RGBA(255, 0, 0, 255), TEXT("0x%02x"), i)) PostQuitMessage(0); } } if((m_lpD3DDevice->EndScene()) != D3D_OK) { sprintf(g_tcError,TEXT("Nelze ukončit vykreslování scény!")); PostQuitMessage(0); } m_lpD3DDevice->Present(NULL, NULL, NULL, NULL);
5. DirectInput
Prodáno 14.10.2014 na www.Kosmas.cz zákazníkovi [email protected], číslo objednávky 591522, UID: 7e15c933-e4b8-4e2a-94a6-c3fd2dffdec9
DIRECTX
189
Pro jednoduchost zobrazujeme ve výše uvedeném výpisu stisknuté klávesy s kódem 1–50. Pokud nahlédnete do zdrojového kódu programu dodaného s touto knihou, tam se detekují klávesy všechny. Při výpisu si všimněte kódů stisknutých kláves. Ty neodpovídají standardnímu kódování ASCII, jak by se možná čekalo, ale rozložení kláves na klávesnici. Tedy klávesa ESCAPE má kód 1, klávesa 1 má kód 2 atd.
5.2 Práce s myší Práci s myší si předvedeme na praktickém příkladě. Na něm si také popíšeme rozdíly oproti práci s klávesnicí. Vytvoříme podobnou aplikaci jako v předchozí kapitole, tentokrát ovšem nebudeme snímat klávesy na klávesnici, ale budeme monitorovat pohyby myši a stisknutí tlačítek na ní. Protože je koncepce programu velice podobná jako u klávesnice, zaměříme se zejména na rozdíly. Soubory v projektu zůstanou stejné, tedy žádný nepřidáme, ani neodebereme. Nezměněny zůstanou soubory CApplication.cpp, CApplication.h, CFont.cpp, CFont.h, CWindow.h a Global.h. Pokud nechceme, nemusíme nijak zasahovat ani do souboru WinMain.cpp. V našem programu jsme udělali pouze drobnou úpravu ve funkci zpracování zpráv. U klávesnice jsme zrušili ošetření klávesy ESCAPE na ukončení programu. Nyní ho tam můžeme opět pro snadnější ukončení celé aplikace vložit. Třída CInput se v souboru CInput.h změnila následovně: class CInput{ private: LPDIRECTINPUT8 m_lpDInput; LPDIRECTINPUTDEVICE8 m_lpDIDMouse; DIMOUSESTATE
m_MouseState;
public: CInput(void); ~CInput(void); bool Initialize(HINSTANCE hInst, HWND hWnd); void Terminate(void);
5. DirectInput
bool RestoreDevice(void); DIMOUSESTATE GetMouse(void) const {return (m_MouseState);} bool GetMouseState(void); }; U atributů jsme vhodněji pojmenovali jméno objektu vstupního zařízení, který bude reprezentovat myš na m_lpDIDMouse. Dále jsme místo pole 256 bytů, kam se ukládaly stavy kláves, vložili instanci struktury DIMOUSESTATE. Do této struktury budeme ukládat stavy myši. Obecná syntaxe této struktury je následující:
5.2 Práce s myší
Prodáno 14.10.2014 na www.Kosmas.cz zákazníkovi [email protected], číslo objednávky 591522, UID: 7e15c933-e4b8-4e2a-94a6-c3fd2dffdec9
190
DIRECTX
typedef struct DIMOUSESTATE { LONG lX; LONG lY; LONG lZ; BYTE rgbButtons[4]; } DIMOUSESTATE, *LPDIMOUSESTATE; Proměnné lX, lY představují přírůstky pohybu myši v osách x a y, tedy změnu polohy kurzoru na obrazovce vzhledem k předchozímu stavu. Podobně je tomu u proměnné lZ, která představuje přírůstek v pomyslné ose z. Ke změně této hodnoty dochází otáčením kolečka myši. Pole bytů rgbButtons představuje stavy stisknutých tlačítek na myši. Nultý index představuje levé tlačítko, první index pravé tlačítko, druhý prostřední tlačítko (nebo kolečko) a třetí boční tlačítko myši – samozřejmě za předpokladu, že všechna tato tlačítka myš má. Některé speciální myši mají tlačítek i více, to se potom používá struktura DIMOUSESTATE2, která se od své předchůdkyně liší tím, že má pole tlačítek 8 bytů, takže se dají uchovávat stavy až osmi tlačítek. Z metod třídy CInput si všimněme nové metody GetMouse, která vrací strukturu stavu myši. S pomocí této metody můžeme v místě programu, odkud tuto metodu voláme, patřičně zareagovat. Poslední metoda GetMouseState je podobná metodě GetKeyboardState z předchozího programu – bude ukládat stavy myši do struktury m_MouseState. V definicích metod třídy CInput se zaměřme na trojici Initialize, RestoreDevice a GetMouseState. Ostatní metody třídy jsou konstruktor, destruktor a metoda Terminate, které provádějí standardní počáteční, respektive ukončovací operace. Metoda Initialize má nyní následující tělo: bool CInput::Initialize(HINSTANCE hInst, HWND hWnd) { if(DirectInput8Create(hInst, DIRECTINPUT_VERSION, IID_IDirectInput8, (void**)&m_lpDInput, NULL) != DI_OK) { sprintf(g_tcError,TEXT("Nelze vytvořit objekt DirectInput8!")); return (false); } if(m_lpDInput->CreateDevice(GUID_SysMouse, &m_lpDIDMouse, NULL) != DI_OK) { sprintf(g_tcError,TEXT("Nelze vytvořit objekt myši!")); return (false); } if(m_lpDIDMouse->SetDataFormat(&c_dfDIMouse) != DI_OK) { sprintf(g_tcError,TEXT("Nelze nastavit formát dat pro myš!")); return (false); }
5. DirectInput
Prodáno 14.10.2014 na www.Kosmas.cz zákazníkovi [email protected], číslo objednávky 591522, UID: 7e15c933-e4b8-4e2a-94a6-c3fd2dffdec9
DIRECTX
191
if(m_lpDIDMouse->SetCooperativeLevel(hWnd, DISCL_FOREGROUND|DISCL_NONEXCLUSIVE) != DI_OK) { sprintf(g_tcError,TEXT("Nelze nastavit chování myši!")); return (false); } if(!RestoreDevice()) return (false); return (true); } V prvním příkazu se vytváří objekt DirectInput. Zde se oproti klávesnici nic nezměnilo. Druhým příkazem se vytváří objekt vstupního zařízení. Protože se nyní bude jednat o myš, první parametr nebude GUID_SysKeyboard, ale GUID_SysMouse. Po vytvoření objektu m_lpDIDMouse se nastavuje formát dat tohoto vstupního zařízení. Zde do metody IDirectInputDevice8::SetDataFormat nastavíme c_dfDIMouse pro klasickou myš. Kdyby měla být vstupním zařízením osmitlačítková myš, přišla by sem hodnota c_dfDIMouse2. Následuje nastavení chování myši zavoláním metody IDirectInputDevice8::SetCooperativeLevel. V této metodě nastavujeme stejné příznaky jako u klávesnice. Kdybychom ale chtěli mít k myši exkluzivní přístup, tedy místo příznaku DISCL_NONEXCLUSIVE bychom zadali DISCL_EXCLUSIVE, museli bychom se o myš důsledněji starat. Museli bychom například zajistit i zobrazování jejího kurzoru na obrazovce. Důvod je ten, že pokud nastavíme výhradní přístup, omezíme tím i některé funkce systému Windows. Nakonec se volá metoda pro přístup k datům CInput::RestoreDevice. Stejně jako u klávesnice se tato přístupnost musí znovu aktivovat při ztrátě vstupního zařízení, což je důvod, proč máme tento kód v samostatné funkci:
if(m_lpDIDMouse->Acquire() != DI_OK) { sprintf(g_tcError,TEXT("Nelze získat přístup k datům myši!")); return (false); }
5. DirectInput
bool CInput::RestoreDevice(void) { if(!m_lpDInput) { sprintf(g_tcError,TEXT("Neexistuje objekt DirectInput8!")); return (false); }
return (true); } Kromě jiného názvu identifikátoru objektu myši nenajdeme v této metodě žádné další rozdíly oproti příkladu s klávesnicí.
5.2 Práce s myší
Prodáno 14.10.2014 na www.Kosmas.cz zákazníkovi [email protected], číslo objednávky 591522, UID: 7e15c933-e4b8-4e2a-94a6-c3fd2dffdec9
192
DIRECTX
Poslední členská metoda třídy CInput, GetMouseState je velice krátká. Voláme v ní pouze metodu IDirectInputDevice8::GetDeviceState pro zjištění stavu myši a uložení tohoto stavu do struktury m_MouseState. bool CInput::GetMouseState(void) { if(m_lpDIDMouse->GetDeviceState(sizeof(m_MouseState), &m_MouseState) != DI_OK) { sprintf(g_tcError,TEXT("Nelze zjistit stav myši!")); return (false); } return (true); } Poslední úpravy v programu se budou týkat metod třídy CWindow. Vlastně jediná změna se bude týkat funkce CWindow::UpdateFrame. Bude se jednat o část výpisu na obrazovku. Následující kód je vytvořen tak, že se nejprve vymaže obrazovka a v horní části se zobrazí text „Stav myši:“. Pod tímto textem se budou zobrazovat přírůstky pohybu myši, otáčení kolečkem, a dále absolutní souřadnice polohy myši na obrazovce. Pod nimi se ještě budou zobrazovat stavy jednotlivých tlačítek. Zde je příslušná část programového kódu: m_lpD3DDevice->Clear(0, NULL, D3DCLEAR_TARGET, D3DCOLOR_XRGB(0, 0, 0), 1, 0); if(!m_lpInput->GetMouseState()) PostQuitMessage(0); DIMOUSESTATE MouseState = m_lpInput->GetMouse(); if((m_lpD3DDevice->BeginScene()) != D3D_OK) { sprintf(g_tcError,TEXT("Nelze začít vykreslovat scénu!")); PostQuitMessage(0); } m_lpFont->SetRectD3DXFont(0, 0, 0, 0); if(!m_lpFont->DrawTextD3DXFont(-1, DT_NOCLIP, D3DCOLOR_RGBA(255, 0, 0, 255), TEXT("Stav myši:"))) PostQuitMessage(0); m_lpFont->SetRectD3DXFont(0, 20, 0, 0); if(!m_lpFont->DrawTextD3DXFont(-1, DT_NOCLIP, D3DCOLOR_RGBA(0, 255, 0, 255), TEXT("Přírustky pohybu myši: X=%3.3d, Y=%3.3d, Z=%3.3d"), MouseState.lX, MouseState.lY, MouseState.lZ)) PostQuitMessage(0); POINT ptCursor; GetCursorPos(&ptCursor); m_lpFont->SetRectD3DXFont(0, 40, 0, 0);
5. DirectInput
Prodáno 14.10.2014 na www.Kosmas.cz zákazníkovi [email protected], číslo objednávky 591522, UID: 7e15c933-e4b8-4e2a-94a6-c3fd2dffdec9
DIRECTX
193
if(!m_lpFont->DrawTextD3DXFont(-1, DT_NOCLIP, D3DCOLOR_RGBA(0, 0, 255, 255), TEXT("Aktuální poloha kurzoru myši: X=%3.3d, Y=%3.3d"), ptCursor.x, ptCursor.y)) PostQuitMessage(0); m_lpFont->SetRectD3DXFont(0, 60, 0, 0); if(!m_lpFont->DrawTextD3DXFont(-1, DT_NOCLIP, D3DCOLOR_RGBA(255, 255, 0, 255), TEXT("Stisknutá tlačítka myši: 1=%c, 2=%c, 3=%c, 4=%c"), (MouseState.rgbButtons[0] & 0x80) ? '1' : '0', (MouseState.rgbButtons[1] & 0x80) ? '1' : '0', (MouseState.rgbButtons[2] & 0x80) ? '1' : '0', (MouseState.rgbButtons[3] & 0x80) ? '1' : '0')) PostQuitMessage(0); if((m_lpD3DDevice->EndScene()) != D3D_OK) { sprintf(g_tcError,TEXT("Nelze ukončit vykreslování scény!")); PostQuitMessage(0); } m_lpD3DDevice->Present(NULL, NULL, NULL, NULL); Uvedli jsme si, že myš si uchovává informace pouze o přírůstcích jejího pohybu. Protože nemáme zajištěný exkluzivní přístup k myši, ke zjištění aktuálních souřadnic myši na obrazovce můžeme použít funkci Win32API GetCursorPos, který tyto souřadnice uloží do struktury ptCursor (typ POINT), jež je jediným parametrem této funkce. Tato struktura má pouze dvě proměnné, kde jsou uloženy souřadnice, tedy v našem případě jsou tyto souřadnice uloženy jako ptCursor.x a ptCursor.y. Pro jednoduchost zobrazení stisknutých tlačítek myši vypisujeme tyto údaje na obrazovku jako 0 (nestisknuto) nebo 1 (stisknuto). Přitom využíváme podmíněný příkaz, který pro první tlačítko vypadá takto:
5. DirectInput
(MouseState.rgbButtons[0] & 0x80) ? '1' : '0'
5.2 Práce s myší
Prodáno 14.10.2014 na www.Kosmas.cz zákazníkovi [email protected], číslo objednávky 591522, UID: 7e15c933-e4b8-4e2a-94a6-c3fd2dffdec9
194
DIRECTX
5. DirectInput
Prodáno 14.10.2014 na www.Kosmas.cz zákazníkovi [email protected], číslo objednávky 591522, UID: 7e15c933-e4b8-4e2a-94a6-c3fd2dffdec9
DIRECTX
195
6. DirectSound a DirectMusic V této kapitole si popíšeme dvě API z DirectX, jež umožňují práci se zvukem a hudbou. Pro obě API platí, že se v poslední době nevyvíjejí, což ovšem neznamená, že jsou zanedbané, neboť i tak disponují poměrně širokou škálou možností a rozšíření, týkajících se hudby a zvuků. Pokud chceme ve svých aplikacích pouze přehrávat hudební a zvukové soubory, nemusíme nutně používat DirectSound, respektive DirectMusic. Pro přehrání zvukových souborů typu *.wav nebo hudebních souborů ve formátu *.mid můžeme využít funkce Win32 API. Pro přehrání souborů typu *.wav stačí zavolat jedinou funkci PlaySound, která může vypadat například takto:
Prvním parametrem funkce PlaySound je jméno přehrávaného souboru, druhý parametr je handle modulu zdroje (pokud se zvuk přehrává ze zdrojů v paměti a nikoliv ze souboru), třetí parametr jsou příznaky přehrávání. SND_FILENAME znamená, že se zvuk bude přehrávat ze souboru a SND_ASYNC říká, že se funkce hned ukončí a během přehrávání zvuku běží program normálně dál.
6. DirectSound a DirectMusic
PlaySound("click.wav", NULL, SND_FILENAME | SND_ASYNC);
6. DirectSound a DirectMusic
Prodáno 14.10.2014 na www.Kosmas.cz zákazníkovi [email protected], číslo objednávky 591522, UID: 7e15c933-e4b8-4e2a-94a6-c3fd2dffdec9
196
DIRECTX
Přehrávání hudebních souborů typu *.mid pomocí Win32 API je o něco složitější. Tady se používá MIDI syntetizátor (převod tónů na zvuky), který bývá standardní součástí zvukových karet. S ním se manipuluje pomocí funkce mciSendCommand, která dokáže zajistit veškeré operace. Nejdříve se s ní MIDI zařízení otevírá a přitom se do něj nahraje i soubor pro přehrání. Potom se již může soubor přehrávat, pozastavit nebo úplně zastavit. Na konci je třeba MIDI zařízení opět zavřít. Vzhledem k tomu, že ukázka by byla trochu delší a tato kniha je věnována DirectX, nebudeme ji zde uvádět ani blíže popisovat. Případní zájemci mohou nahlédnout do dokumentace k Win32 API. DirectSound a DirectMusic pochopitelně nabízejí více než jen přehrávání zvukových a hudebních souborů. Zde je stručný výčet možností DirectSound [14]:
Přehrávání zvuků ze souborů nebo zdrojů (formát *.wav), přičemž je možné přehrávat i více zvuků najednou. DirectSound pracuje se zvuky na nízké úrovni. Díky tomu má vysokou prioritu přístupu ke zvukovému hardwaru za pomocí řízených bufferů. Práce s 3D zvuky, které umožňují lépe vnímat 3D prostředí aplikací. Přidávání určitých zvukových efektů jako je ozvěna nebo pěvecký sbor. Tyto efekty je možné přidávat či měnit dynamicky. Možnost zachytávání zvuku z vnějších vstupů (například z mikrofonu).
Jisté přednosti má i DirectMusic:
Načítání a přehrávání hudby zvuků ve formátech *.mid, *.wav nebo DirectMusic Producer. Přehrávat lze i více skladeb současně. Přehrávání skladeb je možné s vysokou přesností i načasovat. U hudebních souborů je možné programově měnit vlastnosti MIDI zařízení, takže lze měnit například tempo přehrávání. Ke skladbám lze přidávat různé efekty, například ozvěnu. Lze stanovit polohy hudebních a zvukových zdrojů (tvorba prostorových zvuků). K dispozici jsou tzv. Downloadable Sounds (DLS), což je zavedený standard syntetizace zvuku z digitálních hudebních vzorků. Tento standard dává jistotu, že hudební skladby mají na všech počítačích stejný zvukový výstup. Použití i více než 16 MIDI kanálů. V závislosti na limitu syntetizátoru lze přehrávat i více samplů (vzorků) najednou. Možnost zachytávání MIDI dat nebo možnost směrovat tato data z jednoho portu na jiný.
V následujícím textu si teoreticky popíšeme práci s DirectMusic a DirectSound. Pro jednoduchost si popíšeme pouze přehrávání zvuků. Zmíníme se o rozhraních a jejich metodách, které k přehrávání potřebujeme. Vynecháme ale konkrétní programové ukázky. Jednak jsou poměrně rozsáhlé, ale hlavní důvod spočívá v tom, že tyto programové ukázky (nejen přehrávání, ale i dalších činností s hudbou a zvukem) jsou uvedeny v DirectX SDK ve složkách Samples\C++\DirectMusic, respektive Samples\C++\DirectSound. Opisovat je by bylo nošením dříví do lesa. Ke složkám s příklady DirectMusic a DirectSound v DirectX SDK ještě patří malá poznámka. Může se stát, že tam příslušné složky nenaleznete. V tom případě si z internetu stáhně-
6. DirectSound a DirectMusic
Prodáno 14.10.2014 na www.Kosmas.cz zákazníkovi [email protected], číslo objednávky 591522, UID: 7e15c933-e4b8-4e2a-94a6-c3fd2dffdec9
DIRECTX
197
te starší verzi DirectX SDK. Důvod, proč tam mohou některé složky s příklady chybět, je ten, že v poslední době neprošly žádnou aktualizací. Zřejmě z důvodu rozsáhlosti celého balíku DirectX SDK je Microsoft do aktualizací pokaždé nepřidává. Zcela jistě je naleznete v prvním DirectX 9 SDK ze srpna roku 2003.
6.1 DirectSound Pokud chceme pracovat s DirectSound, je třeba do takového programového projektu vložit hlavičkový soubor dsound.h. K takovému programu je nutné přilinkovat knihovnu dsound.lib a v případě potřeby i dxguid.lib. Postup pro přehrání jednoho zvuku pomocí DirectSound můžeme shrnout do několika kroků. Za prvé musíme vytvořit objekt rozhraní IDirectSound8. Na základě takto vytvořeného objektu se vytvoří buffer (objekt rozhraní IDirectSoundBuffer8). Potom se načtou data ze souboru *.wav nebo ze zdrojů aplikace do nějakého pomocného bufferu. Teprve až budou všechna data načtena, mohou se přenést do zvukového bufferu DirectSound. Důvod, proč zvuková data přenášíme nejprve do pomocného bufferu, je ten, že při manipulaci s daty DirectSound bufferu je třeba tento buffer zamknout, tedy mít výhradní přístup, aby se během přenosu nemohly provádět s těmito daty žádné změny. A žádoucí samozřejmě je, aby doba, kdy je DirectSound buffer zamčený, byla co nejkratší. Po přenesení zvukových dat do DirectSound bufferu již můžeme tento zvuk přehrávat či jeho přehrávání kdykoliv zastavit. Přehrávání zvuků je možné nastavit tak, že když se dosáhne konce zvuku, tak se přehrávání automaticky ukončí nebo se zvuk začne cyklicky přehrávat znovu od začátku. Přehrávat je možné i více zvuků najednou, dokonce je možné i přehrávat na různých místech jeden a ten samý zvuk. V tom případě ale musíme stejný zvuk nahrát do jiného DirectSound bufferu, který si pro tyto účely vytvoříme. Jinými slovy: můžeme libovolně kombinovat přehrávání zvuků z různých zvukových bufferů. Zvukový výstup se ze všech bufferů, které se aktuálně přehrávají, automaticky mixuje. Pokud si ale představíme například nějakou počítačovou hru, kde jsou stovky nejrůznějších zvuků, obvykle se řešení z důvodů efektivní práce s pamětí neprovádí tak, že by se pro všechny zvuky vytvářely DirectSound buffery. Načtou se pouze zvuky, které se budou v blízké době přehrávat. Pokud je zřejmé, že v blízké době tyto zvuky nebudou potřeba, po jejich přehrání se do stejného bufferu (jemuž se samozřejmě upraví velikost) načtou jiná zvuková data, která bude možné přehrát, aniž by zabíraly více či méně paměti navíc.
Toto je stručné shrnutí kroků, které jsou potřeba k přehrání zvukového souboru. Pokusme se nyní výše uvedené informace upřesnit. V hierarchii práci se zvuky je nejvýše umístěn objekt rozhraní IDirectSound8, který reprezentuje zvukové zařízení a umožňuje vytvářet zvukové buffery. Tento objekt se v aplikaci vytváří vždy jen jeden. Uvedli jsme si také, že na základě objektu zvukového zařízení se vytváří zvukové buffery, do nichž se ukládají zvuková data (každý zvuk mívá svůj buffer). Objekty těchto bufferů umožňují manipulaci se zvukovými daty, jež jsou v nich uložena. Tyto buffery jsou reprezentovány svými objekty rozhraní lišícími se typem – nejčastěji jde o rozhraní IDirectSoundBuffer8, pro 3D zvuky rozhraní IDirectSound3DBuffer8. Tyto zvukové buffery ovšem bývají často označeny jako sekundární. V aplikaci, kde se používá více zvuků, se ještě obvykle vytváří tzv. primární buffer (jeden pro celou aplikaci). K vytvoření primárního bufferu se často používá rozhraní IDirectSoundBuffer.
6. DirectSound a DirectMusic
Jakmile přestaneme používat zvuky, je třeba zrušit nepotřebné zvukové buffery a na konci aplikace také zrušit objekt rozhraní IDirectSound8.
6.1 DirectSound
Prodáno 14.10.2014 na www.Kosmas.cz zákazníkovi [email protected], číslo objednávky 591522, UID: 7e15c933-e4b8-4e2a-94a6-c3fd2dffdec9
198
DIRECTX
Smyslem primárního bufferu je schopnost kombinace a přehrávání zvuků ze sekundárních bufferů – provádí tedy určitou správu přehrávání různých zvuků. Pro práci s 3D zvuky také existuje rozhraní IDirectSound3DListener8, které se používá pro nastavování a získávání parametrů o poloze posluchače, jeho orientaci a prostředí, ve kterém se má zvuk šířit. Tento buffer se vytváří automaticky pro příslušné zvukové zařízení. Poněkud stranou zůstávají rozhraní efektů – například IDirectSoundFXEcho8 (ozvěna) nebo IDirectSoundFXChorus8 (pěvecký sbor). Stranou stojí proto, že tyto objekty nemusíme vůbec vytvářet, pokud nechceme žádné efekty používat. Pokud tyto efekty používat chceme, aplikují se na každý sekundární buffer zvlášť, protože provádějí transformace zvukových dat v příslušném bufferu. Po výše uvedených teoretických informacích si blíže představíme rozhraní a metody, jež se používají pro přehrávání zvuku. Z důvodu příliš velké rozsáhlosti tohoto tématu si ale uvedeme pouze výběr těch nejpoužívanějších. Po jejich prostudování byste je měli být schopni plně používat. Nezačneme ničím jiným než vytvořením objektu rozhraní IDirectSound8. K tomu se používá funkce DirectSoundCreate8. Její obecná syntaxe vypadá takto: HRESULT DirectSoundCreate8( LPCGUID lpcGuidDevice, LPDIRECTSOUND8 * ppDS8, LPUNKNOWN pUnkOuter ); Prvním parametrem je tzv. GUID, identifikující zvukové zařízení, což může být některá z předdefinovaných hodnot nebo NULL. Druhým parametrem je adresa objektu, který bude v případě úspěšného volání této funkce zvukové zařízení reprezentovat. Třetí parametr je adresa objektu rozhraní IUnkown pro agregaci COM. To se ale v současné době nepoužívá, takže se do této funkce dosazuje NULL. V případě úspěšného vytvoření objektu vrací funkce DirectSoundCreate8 hodnotu DS_OK. Tato hodnota se vrací v případě úspěšného volání drtivé většiny metod DirectSound. Hned po vytvoření tohoto objektu je třeba nastavit vlastnosti zvukového zařízení. To se provádí prostřednictvím metody SetCooperativeLevel: HRESULT IDirectSound8::SetCooperativeLevel( HWND hwnd, DWORD dwLevel ); Prvním parametrem této metody je handle okna aplikace, ke kterému se mají zvuky vztahovat. Druhým parametrem jsou definované vlastnosti zvukového zařízení. K dispozici jsou následující možnosti: DSSCL_EXCLUSIVE pro výhradní přístup ke zvukovému zařízení, DSSCL_NORMAL pro obvyklý přístup (zvukové zařízení se snadněji sdílí s jinými aplikacemi), DSSCL_PRIORITY pro nastavení priority a DSSCL_WRITEPRIMARY pro možnost manipulace s primárním bufferem. Po nastavení vlastností zvukového zařízení můžeme vytvářet zvukové buffery. Vytvoření jednoho bufferu zajistíme zavoláním metody CreateSoundBuffer: HRESULT IDirectSound8:: CreateSoundBuffer( LPCDSBUFFERDESC pcDSBufferDesc, LPDIRECTSOUNDBUFFER * ppDSBuffer, LPUNKNOWN pUnkOuter );
6. DirectSound a DirectMusic
Prodáno 14.10.2014 na www.Kosmas.cz zákazníkovi [email protected], číslo objednávky 591522, UID: 7e15c933-e4b8-4e2a-94a6-c3fd2dffdec9
DIRECTX
199
Jako první nám do této metody vstupuje adresa instance struktury typu DSBUFFERDESC, která obsahuje charakteristiky vytvářeného bufferu. Druhou položkou je adresa objektu rozhraní IDirectSoundBuffer8, který bude vytvořený buffer reprezentovat. Konečně poslední parametr je opět adresa objektu IUnkown rozhraní COM, které se nepoužívá a je tudíž vždy NULL. Struktura DSBUFFERDESC vypadá takto: typedef struct DSBUFFERDESC { DWORD dwSize; DWORD dwFlags; DWORD dwBufferBytes; DWORD dwReserved; LPWAVEFORMATEX lpwfxFormat; GUID guid3DAlgorithm; } DSBUFFERDESC; Zaměřme se především na položky, které nás zajímají nejvíce. Proměnná dwSize obsahuje velikost této struktury v bytech a dwFlags příznaky vytvářeného bufferu. Sem patří například příznak DSBCAPS_PRIMARYBUFFER, pokud má být vytvářený buffer primární, DSBCAPS_CTRLVOLUME, pokud má mít buffer schopnosti s nastavováním hlasitosti atd. Všechny příznaky lze samozřejmě nalézt v DirectX SDK. Do položky dwBufferBytes se ukládá celková požadovaná velikost bufferu v bytech. Poslední položkou, o které se u struktury typu DSBUFFERDESC zmíníme, je lpwfxFormat, jež obsahuje adresu instance struktury typu WAVEFORMATEX, kde jsou uloženy vlastnosti zvukových dat. U primárních bufferů se tato položka nastavuje na NULL. Specifikace formátu tohoto bufferu se provádí až po jeho vytvoření zavoláním metody SetFormat, ta má následující tvar: HRESULT IDirectSoundBuffer8::SetFormat( LPCWAVEFORMATEX pcfxFormat ); Jediným parametrem této funkce je opět adresa struktury typu WAVEFORMATEX s vlastnostmi zvukových dat. Její tvar je následující:
Opět se zmiňme pouze o těch nejdůležitějších položkách. Hned první položka wFormatTag určuje typ formátu souboru. Zvuky uložené v souborech typu *.wav mají nejčastěji formát PCM, v takovém případě se této položce přiřazuje hodnota WAVE_FORMAT_PCM. Následuje počet kanálů. Pokud je nahrávka monofonní, tak se do nChannels ukládá 1, v případě stereofonní nahrávky 2. V následující položce (nSamplesPerSec) se ukládá počet zvukových vzorků za sekundu. Jednotky jsou herzy (Hz), v případě formátu dat PCM přicházejí v úvahu 8000, 11025, 22050 a 44100. Do položky wBitsPerSample se ukládá počet bitů na jeden vzorek, v případě PCM zvuků se sem ukládá hodnota 8 nebo 16. Do položky nBlockAlign se udává velikost dílčích bloků v bytech. U PCM zvuků se sem přiřazuje hodnota wBitsPerSample * wfx.nChannels / 8. Konečně do nAvgBytes-
6. DirectSound a DirectMusic
typedef struct WAVEFORMATEX { WORD wFormatTag; WORD nChannels; DWORD nSamplesPerSec; DWORD nAvgBytesPerSec; WORD nBlockAlign; WORD wBitsPerSample; WORD cbSize; } WAVEFORMATEX;
6.1 DirectSound
Prodáno 14.10.2014 na www.Kosmas.cz zákazníkovi [email protected], číslo objednávky 591522, UID: 7e15c933-e4b8-4e2a-94a6-c3fd2dffdec9
200
DIRECTX
PerSec se ukládá průměrný datový tok bytů za sekundu, což u PCM zvuků odpovídá: nSamplesPerSec * wfx.nBlockAlign. Po vytvoření zvukového bufferu je třeba do něj nějakým způsobem dostat data ze souboru nebo paměťového zdroje. K tomuto účelu je možné použít například funkci CopyMemory, která ale nepatří do DirectSound, takže o ní nebudeme uvádět bližší podrobnosti. Pro nás je důležité vědět, že před kopírováním dat do bufferu je ho potřeba zamknout a po přenesení dat se opět odemkne. Metoda uzamčení vypadá následovně: HRESULT IDirectSoundBuffer8::Lock( DWORD dwOffset, DWORD dwBytes, LPVOID * ppvAudioPtr1, LPDWORD pdwAudioBytes1, LPVOID * ppvAudioPtr2, LPDWORD pdwAudioBytes2, DWORD dwFlags ); První položka udává vzdálenost od začátku bufferu v bytech, kde dojde k prvnímu zamčení. Od tohoto místa se zamkne vše až do položky v bufferu danou součtem této (první) a druhé položky. DwBytes tedy udává množství zamčených dat. Potom následuje adresa proměnné, kam se uloží ukazatel na první zamčenou část bufferu, a do pdwAudioBytes1 se ukládá adresa proměnné, kam se uloží počet bytů v bloku daným ppvAudioPtr1. Následující dva parametry dělají totéž, ale pro druhou uzamčenou část. Všechny tyto čtyři parametry jsou důležité, neboť jsou potřeba při odemykání zvukového bufferu. Pokud máme jen jeden blok na uzamčení, druhou dvojici parametrů můžeme nastavit na NULL. Co přesně se v bufferu uzamkne, to můžeme určit i pomocí příznaků, které jsou posledním parametrem metody Lock. K dispozici jsou dva – DSBLOCK_FROMWRITECURSOR říká, že dojde k zamčení od místa ukazatele (potom se ignoruje první parametr metody Lock), a DSBLOCK_ENTIREBUFFER udává, že se zamkne buffer celý a v tom případě se ignoruje druhý parametr metody Lock. Po úspěšném uzamčení zvukového bufferu můžeme manipulovat s daty v něm, tedy například je tam přenést. Pro přenášení dat do zvukového bufferu je k dispozici nahrávací ukazatel, který specifikuje místo, kam se mohou zvuková data nahrávat. Tento ukazatel se v průběhu ukládání dat do tohoto bufferu automaticky posouvá. Kromě nahrávacího ukazatele je k dispozici ještě jeden ukazatel, který je určen k přehrávání zvuku z tohoto bufferu. Tento přehrávací ukazatel vlastně definuje místo, odkud se bude zvuk přehrávat, a na rozdíl od nahrávacího ukazatele se s ním dá programově manipulovat. Po ukončení práce s daty je třeba buffer zase odemknout metodou Unlock: HRESULT IDirectSoundBuffer8::Unlock( LPVOID pvAudioPtr1, DWORD dwAudioBytes1, LPVOID pvAudioPtr2, DWORD dwAudioBytes2 ); Čtveřice parametrů této metody odpovídá třetímu až šestému parametru při volání metody IDirectSoundBuffer8::Lock. Také bychom se měli zmínit o metodě Restore, která se volá v případě ztráty zvukového bufferu: HRESULT IDirectSoundBuffer8::Restore();
6. DirectSound a DirectMusic
Prodáno 14.10.2014 na www.Kosmas.cz zákazníkovi [email protected], číslo objednávky 591522, UID: 7e15c933-e4b8-4e2a-94a6-c3fd2dffdec9
DIRECTX
201
Tato metoda nemá žádné parametry a v případě úspěchu (vrací se DS_OK) se obnoví paměť přidělená příslušnému zvukovému bufferu. K popisu zvukového bufferu patří ještě výčet některých dalších zajímavých metod pro práci s ním. Jsou uvedeny v tabulce 6.1. V pravé části tabulky je stručně uvedeno, k čemu daná metoda slouží. Cílem je ukázat, že není složité měnit například hlasitost zvuků nebo frekvenci přehrávání. Veškeré podrobnosti ke zde uvedeným metodám včetně jejich parametrů je možné opět najít v DirectX SDK. Tabulka 6.1: Přehled nejdůležitějších metod rozhraní IDirectSoundBuffer8
Metoda IDirectSoundBuffer8::GetCurrentPosition IDirectSoundBuffer8::SetCurrentPosition IDirectSoundBuffer8::Play IDirectSoundBuffer8::Stop IDirectSoundBuffer8::GetFrequency IDirectSoundBuffer8::GetPan IDirectSoundBuffer8::GetVolume IDirectSoundBuffer8::SetFrequency IDirectSoundBuffer8::SetPan IDirectSoundBuffer8::GetVolume IDirectSoundBuffer8::GetCaps IDirectSoundBuffer8::GetFormat
Popis Získání přehrávacího ukazatele bufferu. Nastavení přehrávacího ukazatele v bufferu. Spuštění přehrávání zvuku z bufferu od místa přehrávacího ukazatele. Zastavení přehrávání zvuku zvukového bufferu. Získání frekvence zvukového bufferu. Získání rozdílu hlasitosti mezi levým a pravým zvukovým kanálem. Získání hlasitosti zvuku. Nastavení frekvence zvukového bufferu. Nastavení rozdílu hlasitosti mezi levým a pravým zvukovým kanálem. Nastavení hlasitosti zvuku. Získání vlastností zvukového bufferu. Získání formátu zvukových dat uložených ve zvukovém bufferu.
Na úplný závěr výkladu o DirectSound připomeňme zlaté pravidlo: experimentujte s hotovými programy. V DirectX SDK jich najdete hned několik. Kromě programových ukázek základní manipulace se zvuky naleznete v DirectX SDK také dvojici velice zajímavých souborů – SDKsound.h a SDKsound.cpp. V nich najdete čtyři třídy, samozřejmě i s jejich definovanými metodami. Tyto třídy se jmenují CSoundManager, CSound, CStreamingSound (dědí ze třídy CSound) a CWaveFile. Mohou vám ulehčit mnoho práce se zvuky a zvukovými soubory. Pokud se chcete o DirectSound dozvědět více, vřele vám doporučuji prostudovat právě je.
Pro práci s DirectMusic je třeba vložit hlavičkový soubor Dmusici.h. Většinou je také potřeba přilinkovat knihovnu dxguid.lib. Rozhraní DirectMusic je celkem asi třicet. Názvy těchto rozhraní končí osmičkou. Toto číslo na konci udává verzi rozhraní – osmá verze je v tuto dobu nejnovější. Každé rozhraní má samozřejmě svoje určení a metody. V tabulce 6.2 je uveden přehled a popis vybraných rozhraní DirectMusic, která se často používají.
6. DirectSound a DirectMusic
6.2 DirectMusic
6.2 DirectMusic
Prodáno 14.10.2014 na www.Kosmas.cz zákazníkovi [email protected], číslo objednávky 591522, UID: 7e15c933-e4b8-4e2a-94a6-c3fd2dffdec9
202
DIRECTX Tabulka 6.2: Přehled často používaných rozhraní DirectMusic
Rozhraní IDirectMusic8 IDirectMusicBuffer8 IDirectMusicCollection8 IDirectMusicDownloadedInstrument8 IDirectMusicInstrument8 IDirectMusicLoader8 IDirectMusicPerformance8 IDirectMusicPort8
Popis Správa paměťových bufferů, portů a časování. Paměť pro uložení MIDI zpráv. Správa DLS souborů (syntetizované zvuky). Nástroj, který lze vložit do syntetizátoru. Jeden hudební nástroj z kolekce DLS. Správa hudebních dat (načítání ze souboru apod.). Správa přehrávání, předávání zpráv, konverze apod. Zařízení pro příjem či odesílání hudebních dat.
IDirectMusicSegment8
Zvukový segment, který lze přehrát.
IDirectMusicSegmentState8
Přehrávaná instance segmentu.
K popisu tabulky 6.1 si uveďme ještě několik doplňujících poznámek. V případě formátu MIDI se nepřenáší zvuk jako takový, ale zvuková zařízení, jež spolu komunikují (například elektronický hudební nástroj a počítač), si předávají informace prostřednictvím MIDI zpráv. Tyto zprávy nesou informace o tónech. Tóny se na zvuk převádějí pomocí hardwarového nebo softwarového syntetizátoru. DirectMusic popíšeme pomocí programu pro přehrávání hudby ze souboru typu *.mid. Půjde o jednoduchý příklad, jehož obdobu včetně zdrojového kódu naleznete mezi příklady DirectX SDK ve složce Samples\C++\DirectMusic\Tutorials\Tutorial1. Pro tyto účely budeme tedy potřebovat následující tři rozhraní: IDirectMusicLoader8, IDirectMusicPerformance8 a IDirectMusicSegment8*. V dalším kroku musíme nejprve inicializovat COM rozhraní příkazem: CoInitialize(NULL); Potom již můžeme vytvářet objekty – prozatím jen pro objekty rozhraní IDirectMusicLoader8 a IDirectMusicPerformance8. K vytvoření těchto objektů se používá funkce CoCreateInstance: STDAPI CoCreateInstance( REFCLSID rclsid, LPUNKNOWN pUnkOuter, DWORD dwClsContext, REFIID riid, LPVOID * ppv ); Prvním parametrem této funkce je identifikátor specifikující typ vytvářeného objektu. Druhý parametr se užívá pro agregaci COM, což používat nebudeme, proto budeme na toto místo dosazovat NULL. Následuje kontext, ve kterém má být objekt spuštěn, a reference na identifikátor rozhraní pro využití ke komunikaci s vytvářeným objektem. Posledním parametrem je adresa ukazatele vytvářeného objektu, který bude příslušné rozhraní reprezentovat.
6. DirectSound a DirectMusic
Prodáno 14.10.2014 na www.Kosmas.cz zákazníkovi [email protected], číslo objednávky 591522, UID: 7e15c933-e4b8-4e2a-94a6-c3fd2dffdec9
DIRECTX
203
Následovat bude inicializace vytvořeného objektu rozhraní IDirectMusicPerformance8. Inicializaci provádí metoda InitAudio: HRESULT IDirectMusicPerformance8::InitAudio( IDirectMusic** ppDirectMusic, IDirectSound** ppDirectSound, HWND hWnd, DWORD dwDefaultPathType, DWORD dwPChannelCount, DWORD dwFlags, DMUS_AUDIOPARAMS *pParams ); V případě úspěšné inicializace vrací tato metoda hodnotu S_OK (tato hodnota se vrací v případě úspěšného volání většiny metod rozhraní DirectMusic). První dva parametry představují adresu ukazatelů objektů IDirectMusic a IDirectSound, které se budou při volání této funkce vytvářet. Protože tyto objekty nepotřebujeme, budeme místo těchto dvou parametrů vkládat NULL. Následuje handle okna aplikace. Dále máme specifikaci výstupu. Zde se často používá hodnota DMUS_APATH_SHARED_STEREOPLUSREVERB, která znamená stereo buffer. Potom přichází počet kanálů a příznaky specifikující požadované vlastnosti. K dispozici je několik příznaků s různými vlastnostmi. Pokud chceme všechny běžné vlastnosti, nastavuje se příznak DMUS_AUDIOF_ALL. Posledním parametrem metody InitAudio je adresa instance struktury typu DMUS_AUDIOPARAMS pro specifikaci parametrů syntetizátoru. Pokud tyto hodnoty nechceme zadat, poslední parametr může být také NULL. Po inicializaci můžeme načíst data ze souboru. Přitom vytvoříme objekt hudebního segmentu a naplníme ho zvukovými daty. Tuto činnost provádí metoda IDirectMusicLoader8:: LoadObjectFromFile: HRESULT IDirectMusicLoader8::LoadObjectFromFile( REFGUID rguidClassID, REFIID iidInterfaceID, WCHAR *pwzFilePath, void ** ppObject );
Nyní se dostáváme k přehrávání. Ještě předtím ovšem potřebujeme do syntetizátoru přenést sadu nástrojů pro převod hudebních tónů na zvuk. To provádí metoda Download z rozhraní IDirectMusicSegment8: HRESULT IDirectMusicSegment8::Download( IUnknown* pAudioPath );
6. DirectSound a DirectMusic
Prvním parametrem je unikátní identifikátor třídy objektu. V případě zvukových segmentů je tento identifikátor CLSID_DirectMusicSegment. Následuje unikátní identifikátor typu rozhraní. Pokud budeme chtít vytvořit objekt rozhraní IDirectMusicSegment8, tato hodnota bude IID_IDirectMusicSegment8. Následuje jméno souboru (případně i cesta k němu, pokud by byl umístěn v jiném adresáři – lze načítat soubory *.mid nebo *.wav), ze kterého se data načtou. Posledním parametrem je adresa ukazatele na objekt, který se vytvoří a bude obsahovat načtená data ze souboru. Pokud proběhlo načtení v pořádku, návratová hodnota této metody je S_OK.
6.2 DirectMusic
Prodáno 14.10.2014 na www.Kosmas.cz zákazníkovi [email protected], číslo objednávky 591522, UID: 7e15c933-e4b8-4e2a-94a6-c3fd2dffdec9
204
DIRECTX
Jediným parametrem této metody je ukazatel na rozhraní IUnkown objektu, který má přijmout data. Pro účely přehrávání je potřeba, aby šlo o objekt rozhraní IDirectMusic Performance8, jež bude zajišťovat přehrávání. Spuštění přehrávání zajišťují metody rozhraní IDirectMusicPerformance8 s názvem PlaySegment (základní přehrávání) nebo PlaySegmentEx (s nastavitelnými vlastnostmi). Metoda PlaySegment má následující tvar: HRESULT IDirectMusicPerformance8::PlaySegment( IDirectMusicSegment* pSegment, DWORD dwFlags, __int64 i64StartTime, IDirectMusicSegmentState** ppSegmentState ); Parametry jsou celkem čtyři (PlaySegmentEx jich má osm), ale pro standardní přehrávání je třeba nastavit pouze první parametr. Tím je zvukový segment, který se má přehrát. Druhý a třetí parametr mohou být 0 (jsou to příznaky pro přehrávání a čas, kdy se má začít přehrávat – tento čas je právě závislý na nastavených příznacích). Poslední parametr je ukazatel na objekt rozhraní IDirectMusicSegmentState8, pokud bychom chtěli přehrávaný zvukový segment uchovávat. Není-li to potřeba, je možné na místo tohoto ukazatele dosadit NULL. Pro zastavení přehrávání se používají metody Stop nebo StopEx. Syntaxe metody Stop je následující: HRESULT IDirectMusicPerformance8::Stop( IDirectMusicSegment* pSegment, IDirectMusicSegmentState* pSegmentState, MUSIC_TIME mtTime, DWORD dwFlags ); První dvojici parametrů tvoří ukazatel na zvukové segmenty (objekty rozhraní IDirectMusicSegment a IDirectMusicSegmentState), které se mají zastavit. Třetím parametrem je čas, kdy má k zastavení dojít, a tento čas se dá přesněji specifikovat pomocí příznaků, jež jsou čtvrtým parametrem této metody. Pokud chceme jednouše zastavit okamžité přehrávání, je možné první dva parametry nastavit jako NULL a druhé dva jako 0. Pro uzavření objektu pro přehrávání (je to opak metody InitAudio) se používá metoda CloseDown, která nemá žádné parametry: HRESULT IDirectMusicPerformance8::CloseDown(); Nakonec si demonstrujme praktickou ukázku programového kódu, vytvořeného na základě předchozích informací. Jde o program z DirectX SDK, jak jsme již uvedli výše. Pro jednoduchost chybí vložené hlavičkové soubory a ve volaných funkcích a metodách nejsou testovány návratové hodnoty (což by samozřejmě nemělo chybět). Nicméně tento programový kód je funkční a lze ho implementovat do libovolného programu: IDirectMusicLoader8* lpDMLoader = NULL; IDirectMusicPerformance8* lpDMPerformance = NULL; IDirectMusicSegment8* lpDMSegment = NULL; // inicializace COM CoInitialize(NULL);
6. DirectSound a DirectMusic
Prodáno 14.10.2014 na www.Kosmas.cz zákazníkovi [email protected], číslo objednávky 591522, UID: 7e15c933-e4b8-4e2a-94a6-c3fd2dffdec9
DIRECTX
205
// vytvoření objektů lpDMLoader a lpDMPerformance CoCreateInstance(CLSID_DirectMusicLoader, NULL, CLSCTX_INPROC, IID_IDirectMusicLoader8, (void**)&lpDMLoader); CoCreateInstance(CLSID_DirectMusicPerformance, NULL, CLSCTX_INPROC, IID_IDirectMusicPerformance8, (void**)&lpDMPerformance); // inicializace objektu pro přehrávání lpDMPerformance->InitAudio(NULL, NULL, NULL, DMUS_APATH_SHARED_STEREOPLUSREVERB, 64, DMUS_AUDIOF_ALL, NULL ); // načtení dat ze souboru lpDMLoader->LoadObjectFromFile(CLSID_DirectMusicSegment, IID_ IDirectMusicSegment8, L"music.mid", (LPVOID*) &lpDMSegment) // uložení nástrojů do syntetizátoru lpDMSegment->Download(lpDMPerformance); // spuštění přehrávání lpDMPerformance->PlaySegment(g_pSegment, 0, 0, NULL); // konec přehrávání lpDMPerformance->Stop( NULL, NULL, 0, 0 ); lpDMPerformance->CloseDown(); // na konci programu uvolnění objektů a ukončení COM lpDMLoader->Release(); lpDMLoader=NULL; lpDMPerformance->Release(); lpDMPerformance=NULL; lpDMSegment->Release(); lpDMSegment=NULL;
6. DirectSound a DirectMusic
CoUninitialize();
6.2 DirectMusic
Prodáno 14.10.2014 na www.Kosmas.cz zákazníkovi [email protected], číslo objednávky 591522, UID: 7e15c933-e4b8-4e2a-94a6-c3fd2dffdec9
206
DIRECTX
6. DirectSound a DirectMusic
Prodáno 14.10.2014 na www.Kosmas.cz zákazníkovi [email protected], číslo objednávky 591522, UID: 7e15c933-e4b8-4e2a-94a6-c3fd2dffdec9
207 7. DirectPlay
DIRECTX
7. DirectPlay DirectPlay je komponenta DirectX, která umožňuje vývojářům vytvářet programy pro komunikaci s programy na jiných počítačích. Nutným předpokladem samozřejmě je, aby byly počítače vzájemně propojeny (přímo, nebo prostřednictvím serveru) a mohla tak probíhat komunikace. Tato komunikace probíhá prostřednictvím programů (tzv. klientských aplikací) a tyto klientské aplikace musí být také spuštěné na všech počítačích, které spolu mají komunikovat. Předchozí větu bychom ale měli ještě upřesnit, protože vlastně nekomunikují počítače jako takové, ale aplikace (na jednom počítači může být spuštěno více komunikačních aplikací). Komunikující aplikace, které jsou založeny na DirectPlay, mají několik výhod [14]:
Jednoduchost vytváření síťového připojení, aniž byste museli znát síťovou architekturu do hloubky.
Způsoby komunikace peer-to-peer nebo klient/server.
Možnost správy uživatelů a skupin uživatelů.
7. DirectPlay
Prodáno 14.10.2014 na www.Kosmas.cz zákazníkovi [email protected], číslo objednávky 591522, UID: 7e15c933-e4b8-4e2a-94a6-c3fd2dffdec9
208
DIRECTX
Základní komunikace probíhá prostřednictvím zpráv, které si aplikace posílají mezi sebou přímo, nebo pomocí serveru. Kromě komunikace běžnými zprávami je možné přenášet i zvuková (hlasová) data.
V DirectPlay se dají využít dva způsoby komunikace, které se liší v principu. Již jsme se o nich zmínili – jde o metody peer-to-peer a klient/server. Schémata obou způsobů komunikace pro čtveřici počítačů jsou znázorněna na obrázku 7.1. U peer-to-peer propojení probíhá mezi jednotlivými klientskými aplikacemi přímá komunikace. Pokud si například představíme nějakou síťovou hru, při které podle obrázku 7.1 komunikují čtyři aplikace na čtyřech počítačích, pokud dojde ke změně údajů, jež by se měly projevit i v ostatních aplikacích, tato aplikace posílá o provedené změně trojici zpráv (pro každou z ostatních aplikací jednu). U topologie klient/server zprostředkovává veškerou komunikaci server. Server uchovává informace o jednotlivých klientech a rozesílá zprávy. Pokud se vrátíme k příkladu síťové hry se čtyřmi klientskými aplikacemi, v případě změny údajů, které by se měly projevit i v ostatních aplikacích, se tato zpráva posílá pouze jedna, a to serveru, který poté rozešle zprávy ostatním aplikacím. Veškerá komunikace DirectPlay je založena na tzv. sessions. Tento anglický výraz bývá do češtiny překládán jako „relace“, v této knize se ale budeme držet originálního termínu. Session je komunikační kanál, který musí být sdílen všemi komunikujícími aplikacemi. Jedna aplikace tuto session vytvoří a ostatní se k ní jednoduše připojí. Aplikace, která session vytvoří (někdy se říká, že ji hostuje), ji jako jediná může spravovat a měnit její vlastnosti. V případě peer-to-peer komunikace, kdy aplikace hostující session přestane komunikovat (například dojde k přerušení spojení), se komunikace buď ukončí, nebo se hostování předá jiné aplikaci. V rámci session se pak posílají zprávy. Přehled často užívaných rozhraní DirectPlay s jejich významem je uveden v tabulce 7.1. Po výše uvedených informacích by měla být většina obsahu tabulky srozumitelná. Dosud jsme se však nezmínili o lobby aplikacích a klientech. Smyslem lobby aplikací je zjednodušení vytváření multiplayerových aplikací (her). Lobby je aplikace, která běží na vzdáleném serveru. Uživatel se k této aplikaci připojí a má pak dvě možnosti. Buď nakonfiguruje session a spustí ji – potom se k této session mohou připojovat další uživatelé. Nebo se sám připojí k nějaké session, kterou vytvořil již někdo jiný. Přitom serverová lobby aplikace může spravovat více individuálních aplikací (více různých her stejného typu). K takové serverové aplikaci se lze připojit pomocí lobby klientů. Počítač
Počítač
Počítač
Počítač
Server Počítač
Počítač
Počítač Počítač Obrázek 7.1: Způsoby komunikace peer-to-peer (vlevo) a klient/server (vpravo)
7. DirectPlay
Prodáno 14.10.2014 na www.Kosmas.cz zákazníkovi [email protected], číslo objednávky 591522, UID: 7e15c933-e4b8-4e2a-94a6-c3fd2dffdec9
DIRECTX
209
Rozhraní
Popis
IDirectPlay8Address
Vytváření a manipulace s adresami pro DirectPlay.
IDirectPlay8AddressIP IDirectPlay8Client
Rozhraní užívané pro internetový protokol (IP). Vytváří a spravuje klientskou aplikaci (klient/ server session).
IDirectPlay8LobbiedApplication
Rozhraní užívané v serverových lobby aplikacích.
IDirectPlay8LobbyClient
Rozhraní pro komunikaci s lobby aplikacemi.
IDirectPlay8Peer
IDirectPlayVoiceClient
Vytváří a spravuje aplikaci (peer-to-peer session). Vytváří a spravuje serverovou aplikaci (klient/server session). Správa klientské aplikace (zvuková session).
IDirectPlayVoiceServer
Správa serverové aplikace (zvuková session).
IDirectPlay8Server
7. DirectPlay
Tabulka 7.1: Často užívaná rozhraní DirectPlay a jejich význam
Tolik k teorii DirectPlay. Nyní by měla následovat praktická ukázka. Stejně jako u DirectSound a DirectMusic ovšem nebudeme popisovat konkrétní příklad. Za prvé by byl popis velice rozsáhlý (odlišnou koncepci aplikace vyžaduje komunikace peer-to-peer a klient/server, navíc u komunikace klient/server je třeba rozlišovat klientskou a serverovou aplikaci). Hlavní důvod je ale ten, že v DirectX SDK je uvedeno dostatečné množství programů založených na DirectPlay, samozřejmě i se zdrojovými kódy. Zde bychom je vlastně jen opisovali, což by nemělo smysl. V následujících odstavcích si tak blíže popíšeme rozhraní a jejich základní metody užívané pro komunikaci peer-to-peer a klient/server. Protože nepůjde o konkrétní funkční příklady, ale jen o jejich úseky, vynecháme v ukázkách i testování návratových hodnot globálních funkcí i členských metod, ačkoliv by se toto testování mělo v programech provádět. V případě úspěšného volání vrací většina členských metod hodnotu S_OK. Při vytváření programů používajících DirectPlay je třeba na začátek příslušného souboru vložit hlavičkový soubor dplay8.h a v případě použití DirectPlay lobby ještě navíc dplobby8.h. K přilinkovávaným knihovnám projektu je také potřeba dxguid.lib. Pokud byste chtěli vytvářet programy pod DirectX8, musíte přilinkovávat i knihovnu dplay. lib (pro DirectX9 aplikace se tento soubor přilinkovávat již nemusí).
7.1 Komunikace peer-to-peer Jako první je potřeba vytvořit objekt DirectPlay (objekt rozhraní IDirectPlay8Peer) a provést počáteční inicializaci. Začátek je tak podobný jako u DirectMusic, protože nejdříve potřebujeme inicializovat COM, poté se objekt DirectPlay vytváří funkcí CoCreateInstance. Tuto funkci ovšem budeme volat s jinými parametry. Pro klientskou aplikaci typu peer-to-peer může vypadat volání funkce CoCreateInstance následovně: IDirectPlay8Peer* g_lpDP = NULL; CoInitialize(NULL); CoCreateInstance(CLSID_DirectPlay8Peer, NULL, CLSCTX_INPROC_SERVER, IID_IDirectPlay8Peer, (LPVOID*) &g_lpDP);
7.1 Komunikace peer-to-peer
Prodáno 14.10.2014 na www.Kosmas.cz zákazníkovi [email protected], číslo objednávky 591522, UID: 7e15c933-e4b8-4e2a-94a6-c3fd2dffdec9
210
DIRECTX
Prvním parametrem funkce CoCreateInstance je identifikátor typu vytvářeného objektu, což je u klientské aplikace v topologii peer-to-peer CLSID_DirectPlay8Peer. Druhý argument stejně jako u DirectMusic nepoužíváme a následný kontext, ve kterém má být objekt spuštěn, je specifikován jako CLSCTX_INPROC_SERVER. Identifikátor rozhraní IID_IDirectPlay8Peer je čtvrtý parametr a poslední argument je adresa ukazatele objektu rozhraní IDirectPlay8Peer, který se zavoláním funkce CoCreateInstance vytvoří. Po vytvoření objektu je třeba provést počáteční inicializaci, kterou provádí členská metoda Initialize, jejíž obecná syntaxe je následující: HRESULT IDirectPlay8Peer::Initialize( PVOID const pvUserContext, const PFNDPNMESSAGEHANDLER pfn, const DWORD dwFlags ); Prvním parametrem je ukazatel na uživatelský kontext, který se užívá pro odlišení zpráv, přicházející z různých rozhraní. Potom následuje ukazatel na funkci, jež bude přijímat a zapracovávat zprávy zaslané systémem. Posledním argumentem metody Initialize je kombinace příznaků specifikujících vlastnosti inicializovaného objektu. Zde si udělejme malou odbočku. Zmínili jsme se o funkci zpracovávající zprávy. Tato funkce je obdobou funkce WndProc (zpracování zpráv Windows), kterou jsme používali u předchozích programů. Zde jde ovšem o zprávy zasílané DirectPlay aplikaci. Ve funkci zpracovávající zprávy potom můžeme na přijaté zprávy nějakým způsobem reagovat. Celkem je těchto zpráv více než čtyřicet a jsou různého druhu. O některých z nich se zmíníme v následujících odstavcích. Jejich úplný výčet a specifikace je možné nalézt v DirectX SDK. Pro možnost komunikace je třeba v dalším kroku určit, kteří poskytovatelé komunikačních služeb jsou k dispozici a které je možné pro komunikace použít. Ke zjištění těchto poskytovatelů je možné použít metodu EnumServiceProviders, která navíc dokáže zjistit i poskytované služby: HRESULT IDirectPlay8Peer::EnumServiceProviders( const GUID *const pguidServiceProvider, const GUID *const pguidApplication, const DPN_SERVICE_PROVIDER_INFO *const pSPInfoBuffer, DWORD *const pcbEnumData, DWORD *const pcReturned, const DWORD dwFlags ); Prvním parametrem je identifikátor, který specifikuje službu. Pro internetový protokol IP je tento identifikátor CLSID_DP8SP_TCPIP, pro komunikaci modemem se pak jedná o CLSID_DP8SP_MODEM atd. Následuje ukazatel na zpřístupnění služeb aplikace. Pokud chceme zpřístupnit všechny, vkládá se na toto místo NULL. Třetím argumentem je ukazatel na pole struktur typu DPN_SERVICE_PROVIDER_INFO, kam se uloží informace o poskytovatelích služeb. Následuje ukazatel na proměnnou typu DWORD, kde bude uchována potřebná velikost předchozí položky, pokud by byla paměť vyčleněná argumentem pSPInfoBuffer příliš malá. Protože dopředu obvykle nevíme, jaká bude potřeba paměti pro uložení informací, metoda EnumServiceProviders se v takovém případě volá dvakrát. Při prvním volání se třetí argument této metody nastaví jako NULL a metoda EnumServiceProviders by měla
7. DirectPlay
Prodáno 14.10.2014 na www.Kosmas.cz zákazníkovi [email protected], číslo objednávky 591522, UID: 7e15c933-e4b8-4e2a-94a6-c3fd2dffdec9
vrátit hodnotu DPNERR_BUFFERTOOSMALL (nedostatek paměti pro uložení informací o poskytovateli služeb). Zároveň tím ale získáme ve čtvrtém parametru potřebnou velikost této paměti. Potom se metoda EnumServiceProviders zavolá podruhé a jako třetí parametr se nastaví zjištěná velikost. Pátý parametr po zavolání metody EnumServiceProviders získáme také naplněný. Bude jím číslo udávající počet struktur typu DPN_SERVICE_PROVIDER_INFO, které budou naplněné informacemi o poskytovatelích služeb. Poslední (šestý) argument jsou příznaky – v současné době je k dispozici pouze jeden – DPNENUMSERVICEPROVIDERS_ALL, který zajistí vyčíslení všech poskytovatelů komunikačních služeb v systému.
211 7. DirectPlay
DIRECTX
V dalším kroku je třeba rozhodnout, zda aplikace bude hostovat session (vytvoří ji) nebo se k nějaké jiné, již existující session připojí. V případě hostování session je třeba nejprve vytvořit objekt adresy zařízení, které bude session hostovat (tato adresa bude session reprezentovat a skrze ní se budou připojovat jiné aplikace). Vytvoření adresy může proběhnout například takto: IDirectPlay8Address* g_lpDeviceAddress = NULL; CoCreateInstance(CLSID_DirectPlay8Address, NULL, CLSCTX_INPROC_SERVER, IID_IDirectPlay8Address, (LPVOID*) &g_lpDeviceAddress); g_lpDeviceAddress->SetSP(&CLSID_DP8SP_TCPIP); Pro uchování adresy slouží objekt rozhraní IDirectPlay8Address. Tento objekt vytvoříme opět pomocí funkce CoCreateInstance s parametry, které vidíme ve výše uvedeném výpisu. Po vytvoření tohoto objektu se specifikuje poskytovatel služeb metodou SetSP, což je ve výše uvedeném příkladu internetový protokol IP. Následně již můžeme vytvořit session. To provádí metoda Host, která je členem rozhraní IDirectPlay8Peer: HRESULT IDirectPlay8Peer::Host( const DPN_APPLICATION_DESC *const pdnAppDesc, IDirectPlay8Address **const prgpDeviceInfo, const DWORD cDeviceInfo, const DPN_SECURITY_DESC *const pdpSecurity, const DPN_SECURITY_CREDENTIALS *const pdpCredentials, VOID *const pvPlayerContext, const DWORD dwFlags ); První parametr je ukazatel na strukturu typu DPN_APPLICATION_DESC, jež popisuje vytvářenou session. Obsahuje asi dvanáct položek, které je možné naplnit – například název session, unikátní kód pro specifikaci konkrétní aplikace (odlišení od jiných aplikací) nebo heslo, které je třeba vložit, aby se k této session dalo připojit. Druhý argument je ukazatel na pole objektů typu IDirectPlay8Address, tedy adresy komunikačního zařízení pro hostovanou session. Počet těchto adres je uložen ve třetím argumentu. Čtvrtý a pátý parametr jsou rezervované a jsou vždy NULL. Šestý parametr je ukazatel na proměnnou, kde jsou uloženy informace o uživateli aplikace. V případě, že tyto informace nejsou k dispozici, je možné nastavit tento parametr jako NULL. Posledním argumentem je buď 0, nebo příznak DPNHOST_OKTOQUERYFORADDRESSING pro zobrazení standardního dialogového okna DirectPlay s více informacemi o této metodě. Struktura typu DPN_APPLICATION_DESC má kromě jiných i položku dwFlags s nastavením příznaků. Pokud nastavíme příznak DPNSESSION_MIGRATE_HOST, docílíme toho, že
7.1 Komunikace peer-to-peer
Prodáno 14.10.2014 na www.Kosmas.cz zákazníkovi [email protected], číslo objednávky 591522, UID: 7e15c933-e4b8-4e2a-94a6-c3fd2dffdec9
212
DIRECTX
v případě, kdy chceme ukončit session v aplikaci, která tuto session vytvořila a spravuje (lze to udělat pomocí metody IDirectPlay8Peer::TerminateSession), automaticky se neukončí session jako taková. Takže ostatní aplikace mohou dále komunikovat s tím, že DirectPlay vybere jednu z nich jako nového hostitele session. V té chvíli se v každé aplikaci připojené k takové session vygeneruje systémová zpráva DPN_MSGID_HOST_MIGRATE. U této zprávy jsou připojeny informace o novém hostiteli. Pokud se má aplikace k nějaké již existující session připojit, postup je odlišný. Potřebujeme adresu počítače s aplikací hostující session. Jako první ale musíme vytvořit objekt adresy zařízení pro komunikaci, což je stejné jako v případě aplikace, která session vytváří (tento postup je uveden výše). Kromě toho ale ještě musíme vytvořit objekt adresy pro hostování. V případě IP (internetové komunikace) se této adrese přidá jedinečná (IP) adresa počítače, která session zřídila. Programová ukázka těchto činností je zde: IDirectPlay8Address* g_lpHostAddress = NULL; WCHAR* pwszHost = "128.128.128.128"; CoCreateInstance(CLSID_DirectPlay8Address, NULL, CLSCTX_INPROC_SERVER, IID_IDirectPlay8Address, (LPVOID*) &g_lpHostAddress); g_lpHostAddress->SetSP(&CLSID_DP8SP_TCPIP); g_lpHostAddress->AddComponent(DPNA_KEY_HOSTNAME, pwszHost, *(wcslen(pwszHost) + 1), DPNA_DATATYPE_STRING); Objekt adresy pro hostování se vytváří velice podobně jako objekt adresy komunikačního zařízení. Jde o objekt rozhraní IDirectPlay8Address a vytváří se pomocí funkce CoCreateInstance. Poté se ještě specifikuje poskytovatel služeb voláním metody SetSP. Nové pro nás je volání metody AddComponent, která nastaví IP adresu hostujícího počítače (tato adresa je uložena v ukazateli na pole „širokých“ znaků pwszHost. Tím, že nastavíme pevnou adresu, specifikujeme přímo počítač, kde se má session hledat. Kdybychom toto neprovedli, musely by se prohledávat všechny počítače připojené ve stejné síti. Prvním parametrem metody AddComponent je identifikátor komponenty, která je přidávána. Za ním následuje ukazatel na buffer, kde jsou uložena příslušná data přidávané komponenty. Třetí argument udává velikost dat v bufferu daným předchozím parametrem (v bytech) a poslední (čtvrtý) je identifikátor typu dat. Řetězce ukončeny nulou jsou charakterizovány jako DPNA_DATATYPE_STRING, data v binárním formátu DPNA_DATATYPE_BINARY atd. Jakmile máme obě adresy (adresu komunikačního zařízení a adresu s hostující session), snažíme se vyhledat hostující session. K tomu slouží metoda EnumHosts: HRESULT IDirectPlay8Peer::EnumHosts( PDPN_APPLICATION_DESC const pApplicationDesc, IDirectPlay8Address *const pdpaddrHost, IDirectPlay8Address *const pdpaddrDeviceInfo, PVOID const pvUserEnumData, const DWORD dwUserEnumDataSize, const DWORD dwEnumCount, const DWORD dwRetryInterval, const DWORD dwTimeOut, PVOID const pvUserContext, HANDLE *const pAsyncHandle, const DWORD dwFlags
7. DirectPlay
Prodáno 14.10.2014 na www.Kosmas.cz zákazníkovi [email protected], číslo objednávky 591522, UID: 7e15c933-e4b8-4e2a-94a6-c3fd2dffdec9
DIRECTX
213
Prvním parametrem je ukazatel na strukturu typu DPN_APPLICATION_DESC, o které jsme se již zmiňovali. V této struktuře se nachází popis session, k níž se má připojit. Následuje dvojice ukazatelů na objekty adres pro hostování a komunikačního zařízení. Další dvojice argumentů jsou ukazatel na blok dat, která se mají poslat aplikaci hostující session, a množství těchto dat (počet bytů). Šestý a sedmý argument definují, kolikrát budou tato data odeslána a časový interval v milisekundách mezi odeslanými daty. Osmý parametr specifikuje počet milisekund, jak dlouho se má čekat na odezvu po odeslání posledních dat. Následuje uživatelský kontext, který se používá pro rozlišení jednotlivých aplikací, jež se k session připojují. Předposlední parametr je handle, jež se po zavolání metody EnumHosts naplní hodnotou, kterou lze užít pro přerušení této funkce pomocí metody IDirectPlay8Peer::CancelAsyncOperation. Pokud není toto handle potřeba, parametr může být NULL. Posledním argumentem může být kombinace několika příznaků. Z nich se často používá DPNENUMHOSTS_SYNC pro synchronizovaný průběh této metody.
7. DirectPlay
);
V popisu metody EnumHosts jsme hovořili o zasílání dat. Toto zaslání dat se v hostitelské aplikaci projeví vznikem systémové zprávy DPN_MSGID_ENUM_HOSTS_QUERY, na kterou můžeme samozřejmě ve funkci zpracovávající zprávy reagovat. Reakce se provádí na základě dat připojených ke každé zprávě – je to vlastně struktura, jejíž popis je uveden v DirectX SDK. Po prozkoumání těchto doplňujících informací může hostitelská aplikace reagovat vrácením hodnoty DNP_OK v případě, že je vše v pořádku, nebo jiné hodnoty pro zamítnutí možnosti připojení. Pokud hostitelský počítač odpoví kladně, tak aplikace, která se chtěla připojit, obdrží systémovou zprávu DPN_MSGID_ENUM_HOSTS_RESPONSE s doplňujícími informacemi ve struktuře, která je součástí této zprávy. V další fázi se již můžeme připojit k hostitelské session. K tomu slouží metoda Connect, která vypadá následovně: HRESULT IDirectPlay8Peer::Connect( const DPN_APPLICATION_DESC *const pdnAppDesc, IDirectPlay8Address *const pHostAddr, IDirectPlay8Address *const pDeviceInfo, const DPN_SECURITY_DESC *const pdnSecurity, const DPN_SECURITY_CREDENTIALS *const pdnCredentials, const void *const pvUserConnectData, const DWORD dwUserConnectDataSize, void *const pvPlayerContext, void *const pvAsyncContext, DPNHANDLE *const phAsyncHandle, const DWORD dwFlags ); První trojice parametrů je stejného typu jako u metody EnumHosts. První je ukazatel na strukturu typu DPN_APPLICATION_DESC, která popisuje aplikaci. Následuje ukazatel na objekt rozhraní IDirectPlay8Address, který obsahuje adresné informace pro připojení k počítači, na němž běží aplikace, která hostuje session, k míž se chceme připojit. Třetím argumentem je také ukazatel na objekt rozhraní IDirectPlay8Address, který tentokrát reprezentuje síťový adaptér sloužící pro připojení k jiným počítačům. Následující dvojice parametrů (čtvrtý a pátý) je rezervovaná a oba musí být NULL. Šestý parametr je ukazatel na data, která se při připojování k session odesílají hostitelskému počítači (informace, které chceme sdělit). V těchto datech jsou obvykle uloženy informace o připojující se aplikaci, případně informace o uživateli.
7.1 Komunikace peer-to-peer
Prodáno 14.10.2014 na www.Kosmas.cz zákazníkovi [email protected], číslo objednávky 591522, UID: 7e15c933-e4b8-4e2a-94a6-c3fd2dffdec9
214
DIRECTX
Při připojování se na hostitelském počítači vygeneruje systémová zpráva DPN_MSGID_INDICATE_CONNECT. Zaslaná data se nachází v položce pvUserConnectData struktury, která je součástí této zprávy. Množství (velikost) těchto dat udává sedmý argument metody Connect. Další argument je ukazatel na kontextovou hodnotu uživatele. Tato hodnota se získá ze struktury systémové zprávy DPN_MSGID_CREATE_PLAYER. Tento parametr je volitelný, takže může být i NULL. Následuje ukazatel na uživatelský kontext, který lze získat ze struktury systémové zprávy DPN_MSGID_CONNECT_COMPLETE. Ani tento argument nemusí být specifikován a může být NULL. Poslední dva parametry jsou stejné jako u metody EnumHosts. Předposlední parametr je handle, které je možné použít pro přerušení této funkce pomocí metody IDirectPlay8Peer::CancelAsyncOperation. Tento parametr může být i NULL. V posledním parametru se nastavují příznaky průběhu metody. Pokud má být připojení synchronní, je možné na tomto místě vložit příznak DPNCONNECT_SYNC. V tomto případě ale musí být předposlední parametr NULL, protože přerušit lze pouze asynchronní komunikaci. Jak jsme uvedli, v aplikaci, která hostuje session, se při připojování vygeneruje zpráva DPN_MSGID_CREATE_PLAYER. Pokud je připojení akceptováno, generuje se v aplikaci, která se připojuje, zpráva DPN_MSGID_CONNECT_COMPLETE a ve všech ostatních aplikacích, jež jsou k session již připojeny, se navíc vygeneruje zpráva DPN_MSGID_CREATE_ PLAYER. Na základě těchto zpráv se dají ošetřit veškeré reakce v aplikacích, připojených ke stejné session. Po připojení k session již mohou aplikace vzájemně komunikovat. Uvedli jsme si, že komunikace probíhá prostřednictvím zpráv. Zprávy můžeme zasílat konkrétní aplikaci nebo všem aplikacím, které jsou připojeny ke stejné session. Posílání zpráv zajišťuje metoda SendTo: HRESULT IDirectPlay8Peer::SendTo( const DPNID dpnid, const DPN_BUFFER_DESC *const pBufferDesc, const DWORD cBufferDesc, const DWORD dwTimeOut, void *const pvAsyncContext, DPNHANDLE *const phAsyncHandle, const DWORD dwFlags ); První argument této zprávy je identifikátor aplikace, jež je připojená ke stejné session, které se má zaslat zpráva. Pokud ji chceme poslat všem aplikacím, je možné vložit tento parametr jako DPNID_ALL_PLAYERS_GROUP. Jako druhý parametr máme ukazatel na strukturu typu DPN_BUFFER_DESC. Tato struktura má pouze dvě položky – data a množství těchto dat. Do této struktury se ukládají data, která chceme posílat. Struktur typu DPN_BUFFER_ DESC s posílanými daty může být ve skutečnosti více (až osm). Jejich počet udává třetí parametr. Následuje čas v milisekundách udávající, za jak dlouho se má zpráva poslat. Pátý argument metody SendTo je ukazatel na uživatelský kontext, který je možné získat ze struktury, jež je součástí systémové zprávy DPN_MSGID_SEND_COMPLETE (tato zpráva se generuje v případě, kdy je splněn požadavek na asynchronní zaslání zprávy). Pokud tento kontext nepotřebujeme, může být tento parametr nastaven jako NULL. Předposlední parametr známe z předchozích metod. Jde o ukazatel na handle, jež se vkládá do metody CancelAsyncOperation pro přerušení průběhu metody SendTo. Ovšem tato metoda
7. DirectPlay
Prodáno 14.10.2014 na www.Kosmas.cz zákazníkovi [email protected], číslo objednávky 591522, UID: 7e15c933-e4b8-4e2a-94a6-c3fd2dffdec9
DIRECTX
215
Výše uvedeným způsobem se tedy zpráva posílá. A jak aplikace zjistí, že jí byla zpráva zaslána? Ve chvíli přijetí zprávy se v aplikaci vygeneruje systémová zpráva DPN_MSGID_ RECEIVE. Ve struktuře u této zprávy je položka pReceiveData, což je ukazatel na paměť, kde jsou přijatá data uložena. Na základě těchto dat potom může program podle potřeby reagovat.
7. DirectPlay
musí být volána bez požadavku synchronizace. Právě synchronizace a jiné vlastnosti pro poslání zprávy se nastavují v posledním argumentu metody SendTo.
Na konci programu bychom neměli zapomenout komunikaci uzavřít metodou Close: HRESULT IDirectPlay8Peer::Close( const DWORD dwFlags ); Jediným parametrem této metody jsou příznaky, přesněji řečeno příznak, neboť v současné době je k dispozici pouze jeden. Je jím DPNCLOSE_IMMEDIATE pro okamžité ukončení bez čekání na dokončení provedení této metody. Samozřejmě potom ještě uvolníme všechny vytvořené objekty rozhraní DirectPlay.
7.2 Komunikace klient/server Při tomto způsobu komunikace musíme rozlišovat dva druhy aplikací – klientskou a serverovou. Každá z nich bude mít poněkud odlišnou programovou koncepci. V následujících odstavcích si koncepci obou typů programů popíšeme s tím, že vynecháme podrobnější popis funkcí a členských metod. Tyto údaje jsou podobné jako u komunikace peer-to-peer. Začneme serverem. Zde bychom mohli nastavení rozdělit do tří kroků. Nejprve se musí vytvořit objekt DirectPlay Server. Potom se musí vytvořit objekt adresy a následně se již může spustit hostování. Objekt serveru je vlastně instance rozhraní IDirectPlay8Server. Pro vytvoření objektu serveru můžeme opět použít funkci CoCreateInstance. Jakmile je tento objekt vytvořen, je potřeba ještě provést jeho počáteční inicializaci (metoda Initialize). Za předpokladu, že se metoda pro zpracování systémových zpráv bude jmenovat DirectPlayMessageHandlerServer, by tento programový kód mohl vypadat následovně: IDirectPlay8Server *g_lpDPServer = NULL; CoCreateInstance(CLSID_DirectPlay8Server, NULL, CLSCTX_INPROC_SERVER, IID_IDirectPlay8Server, (LPVOID*) &g_lpDPServer); g_lpDPServer->Initialize(NULL, DirectPlayMessageHandlerServer, 0); V dalším kroku musíme vytvořit objekt adresy. Toto je úplně stejné jako u komunikace peer-to-peer. Jde o objekt rozhraní IDirectPlay8Address, který opět vytvoříme pomocí funkce CoCreateInstance a následně zavoláme členskou metodu SetSP pro nastavení typu komunikace. Posledním krokem je hostování session. To se provádí voláním metody IDirectPlay8Server::Host s příslušnými parametry. Tyto parametry jsou stejného typu jako u stejnojmenné metody rozhraní IDirectPlay8Peer. Abychom se neopakovali, zde si je již nebudeme znovu uvádět. Nastavení aplikací klientů je poněkud více. Jde ale zase o obdobu aplikací v komunikaci peer-to-peer, které se připojovaly k existující session. Nejdříve musíme vytvořit objekt kli-
7.2 Komunikace klient/server
Prodáno 14.10.2014 na www.Kosmas.cz zákazníkovi [email protected], číslo objednávky 591522, UID: 7e15c933-e4b8-4e2a-94a6-c3fd2dffdec9
216
DIRECTX
enta. Potom musíme vytvořit objekty adres klienta a serveru. Zjistíme, jaké session existují, potom se můžeme připojit. Objekt klienta je instance rozhraní IDirectPlay8Client. Jako obvykle můžeme pro vytvoření tohoto objektu použít funkci CoCreateInstance a po jeho vytvoření zavoláním metody Initialize provést počáteční nastavení. Pro název systémové funkce DirectPlayMessageHandlerClient, která bude zpracovávat zprávy, by tato část programového kódu mohla vypadat následovně: IDirectPlay8Client * g_lpDPClient = NULL; CoCreateInstance(CLSID_DirectPlay8Client, NULL, CLSCTX_INPROC_SERVER, IID_IDirectPlay8Client, (LPVOID*) &g_lpDPClient); g_lpDPClient->Initialize(NULL, DirectPlayMessageHandlerClient, 0); Potom vytvoříme dvojici objektů adres pro klienta a server. V obou případech půjde o objekty rozhraní IDirectPlay8Address, které se vytváří. a u nich se nastaví typ komunikace metodou SetSP. Opět jde o stejné činnosti, jež jsme si popsali u peer-to-peer komunikace, když se má aplikace připojovat k session. V dalším kroku následuje hledání serveru s existující session, ke které se chce aplikace připojit. K tomu slouží metoda IDirectPlay8Client::EnumHosts, jež má zcela identické parametry jako stejnojmenná metoda EnumHosts rozhraní IDirectPlay8Peer, jejíž popis jsme si uvedli výše. Nakonec se jako příprava provádí připojení klienta k serveru. K tomu slouží metoda I DirectPlay8Client::Connect. Jde o obdobu dříve popsané metody IDirectPlay8Peer::Connect. Jejich parametry jsou téměř identické, pouze chybí jeden z nich – jde o parametr pvPlayerContext. Jakmile jsou klient a server nakonfigurovány a připraveny ke komunikaci, mohou si vzájemně posílat zprávy. Serverová aplikace posílá zprávy klientům metodou IDirectPlay8Server::SendTo. Parametry této metody jsou opět stejné jako u metody IDirectPlay8 Peer::SendTo. Pokud server zprávu od klientské aplikace naopak přijímá, na serveru se generuje systémová zpráva DPN_MSGID_RECEIVE. Ve struktuře u této zprávy se nachází data, na něž můžeme v aplikaci reagovat. Podobně se pracuje se zprávami u klientských aplikací. Zde se serveru posílá zpráva pomocí IDirectPlay8Client::Send. Parametry jsou opět podobné jako u metod SendTo rozhraní IDirectPlay8Peer a IDirectPlay8Server. Chybí pouze první parametr. Tento parametr (dpnid) slouží k identifikaci příjemce zprávy. Ale protože server je v session pouze jeden, je zbytečný. Přijetí zprávy u klienta je stejné jako u serveru, vygeneruje se tedy zpráva DPN_MSGID_RECEIVE a z připojené struktury lze získat obsah této zprávy. Uzavření komunikace u klientské aplikace či serveru se provádí metodami IDirectPlay8Client::Close, respektive IDirectPlay8Server::Close. Obě tyto metody mají jediný parametr, kterým je buď 0, nebo příznak DPNCLOSE_IMMEDIATE pro okamžité ukončení.
7. DirectPlay
Prodáno 14.10.2014 na www.Kosmas.cz zákazníkovi [email protected], číslo objednávky 591522, UID: 7e15c933-e4b8-4e2a-94a6-c3fd2dffdec9
DIRECTX
8. Co dál?
8.
217
Co dál? Jsme na konci knihy. Jejím cílem bylo poskytnout ucelené informace o jednotlivých komponentách DirectX. Moji snahou bylo vysvětlit základní principy a algoritmy, které můžete ve svých aplikacích použít. Pokud byl pro vás text dostatečně srozumitelný (byť třeba vyvolal další otázky) a získali jste díky němu alespoň částečné poznatky o programování v DirectX, kniha splnila svůj účel. V předchozím odstavci je zmínka o „základních principech“. Je to zcela úmyslně, protože ve skutečnosti je v knize uvedeno poměrně malé procento veškerých informací o DirectX. Mnoho vět v knize obsahovalo text podobný tomuto: „Více informací je možné nalézt v DirectX SDK.“ Skutečně, celé DirectX je velice rozsáhlé. Tato kniha by mohla mít mnohonásobně větší rozsah, přesto by neobsáhla všechno. Jak ale bylo také zmíněno, důraz byl kladen především na pochopení problematiky. Jsem přesvědčen, že pokud jste pochopili popis a logické souvislosti u jednotlivých komponent DirectX, nebude pro vás velkou překážkou programy rozšiřovat i o další prvky, které nejsou v této knize uvedeny. Vodítkem za dalšími informacemi a poznatky pro vás mohou být zdroje uvedené na konci této kapitoly, ze kterých jsem (kromě svých vlastních zkušeností) čerpal poznatky. Uvedené odkazy jsou různého druhu – od stránek zabývajících se přímo DirectX až po knihy
8. Co dál?
Prodáno 14.10.2014 na www.Kosmas.cz zákazníkovi [email protected], číslo objednávky 591522, UID: 7e15c933-e4b8-4e2a-94a6-c3fd2dffdec9
218
DIRECTX
věnované programování v C++ nebo tvorbě počítačových her obecně. Svět programování a DirectX je pro vás otevřen a je tedy jen na vás, zda a jak budete pokračovat dál. Největší část této knihy (více než polovina) je právem věnována Direct3D. Tato část DirectX se rozvíjí nejdynamičtěji a v posledních verzích DirectX prochází nejvíce změnami. Protože jsem nechtěl zabíhat příliš podrobně do matematiky (ačkoliv pokud to s programováním grafiky myslíte vážně, stejně vás to nemine), text je mnohdy značně zjednodušen. Příkladem jsou chybějící výpočty pro nastavení normál, které – jak víme – hrají důležitou roli při výpočtu barvy jednotlivých bodů ploch při renderování. V textu jsme jednoduše předpokládali, že tyto normály (stejně jako například uv souřadnice textury) jsou součásti souboru *.x, což ale nemusí být pravidlem. Samostatnou otázkou je potom získávání modelů a textur. Dnes se programy téměř výhradně vytvářejí v programátorském týmu. Množství členů týmu závisí na velikosti projektu. Zatímco jedna skupina se stará o tvorbu modelů, jiná skupina zase pracuje na animacích. Další skupiny se zabývají kreslením textur, programováním, designem prostředí aplikace atd. Pokud však zatím jen zkoušíte svoje schopnosti a dovednosti, chcete si zkusit vytvořit nějaký menší program (například počítačovou hru) sami, nemusíte vynakládat velké investice do grafických programů a vývojových nástrojů. Pro tvorbu 3D modelů si vystačíte například s programem Blender, který je zcela zdarma a umí exportovat i modely ve formátu *.x. Ostatně modely u programů, které jsou součástí této knihy, byly vytvářeny právě v Blenderu. Stejně tak můžete pro kreslení textur použít bezplatný grafický program Gimp [18], který disponuje mnoha nástroji, jež jsou často součástí drahých grafických programů. Dále, pokud se chcete vyhnout základům programování, můžete využít již nějaký hotový grafický engine. Jeden z těch lepších, který je bezplatný a podporuje i DirectX 9, je Irrlicht engine [20]. A takto bychom mohli pokračovat dále. Nástrojů, které usnadní tvorbu nejen grafických aplikací a počítačových her, se dá nalézt opravdu hodně. Jde jen o to, vybrat si ten „správný“ směr. Na závěr dovolte, abych vám popřál hodně úspěchů při programování nejen v DirectX.
8.1 Použité zdroje [1]
KRUGLINSKI, David J. Mistrovství ve Visual C++. 1. vyd. Praha : Computer Press, 1999. 854 s. ISBN 80-7226-132-0.
[2]
LIBERTY, Jesse. Naučte se C++ za 21 dní. 1. vyd. Praha : Computer Press, 2002. 766 s. ISBN 80-7226-774-4.
[3]
MORRISON, Michael. Naučte se programovat počítačovou hru za 24 hodin. 1. vyd. Brno : Computer Press, 2004. 421 s. ISBN 80-251-0371-4.
[4]
PETZOLD, Charles. Programování ve Windows. 1. vyd. Praha : Computer Press, 1999. 1216 s. ISBN 80-7226-206-8.
[5]
POKORNÝ, Pavel. Blender – naučte se 3D grafiku. 1. vyd. Praha : BEN – technická literatura, 2006. 243 s. ISBN 80-7300-203-5.
[6]
PROSISE, Jeff. Programování ve Windows pomocí MFC. 1. vyd. Praha : Computer Press, 2000. 1133 s. ISBN 80-7226-309-9.
8. Co dál?
Prodáno 14.10.2014 na www.Kosmas.cz zákazníkovi [email protected], číslo objednávky 591522, UID: 7e15c933-e4b8-4e2a-94a6-c3fd2dffdec9
[7]
SIMON, Richard J., GOUKER, Michael, BARNES, Brian C. Win32 API – průvodce vývojáře. 1. vyd. Brno : UNIS Publishing s.r.o., 1997. 2 sv. (669, 748 s.). ISBN 8086097-06-4.
[8]
WALNUM, Clayton. Programujeme grafiku v Direct3D. 1. vyd. Brno : Computer Press, 2004. 358 s. ISBN 80-251-0136-3.
[9]
ŽÁRA, Jiří, BENEŠ, Bedřich, FELKEL, Petr. Moderní počítačová grafika. 1. vyd. Praha : Computer Press, 1998. 448 s. ISBN 80-7226-049-9.
[10] Blender.org – Home [on-line]. Blender Foundation, 2007, 2007-08-16 [cit. 2007-0816]. Dostupné na webu: . [11] Bloodshed software – Dev-C++ [on-line]. Bloodshed Software, 2007 [cit. 2007-0816]. Dostupný na webu: .
219
8. Co dál?
DIRECTX
[12] Borland: C++ Builder 2006 [on-line]. Borland Software Corporation, 1994–2005 [cit. 2007-08-16]. Dostupné na webu: . [13] DirectX [on-line]. Wikimedia Foundation, Inc. [2006], 14 August 2007 [cit. 2007-0816]. Dostupné na webu: . [14] DirectX Resource Center [on-line]. Microsoft Corporation, 2007 [cit. 2007-08-16]. Dostupné na webu: . [15] DOSTÁL, Radim. Seriál Objektově orientované programování v C++ [on-line]. Grafika Publishing, s.r.o., 1997–2004 , 2002-07-04 [cit. 2007-08-16]. Dostupné na webu: . [16] FORMÁNEK, Jiří. Seriál Kurz DirectX [CD-ROM]. Elektronická příloha časopisu CHIP, Vogel Burda Communications, 2001–2007 , 2007-01-03 [cit. 2007-08-16]. Adresář: /chplus/programovani/cecko/dx.htm. [17] GameDev.net – All Your Game Development Needs [on-line]. Gamedev.net, 19992007, 2007-08-15 [cit. 2007-08-15]. Dostupné na webu: . [18] GIMP – the GNU Image Manipulation Program [on-line]. The GIMP Team, 2001– 2007, 2007-08-16 [cit. 2007-08-16]. Dostupné na webu: . [19] CHALUPA, Radek. Seriál Učíme se WinAPI [on-line]. Grafika Publishing, s.r.o., 1997–2004 , 2003-03-21 [cit. 2007-08-16]. Dostupné na webu: . [20] Irrlicht Engine – A Free Open Source 3d Engine [on-line]. 2002–2007, 2007-07-29 [cit. 2007-08-16]. Dostupné na webu: . [21] Microsoft Visual Studio [on-line]. Microsoft Corporation, 2007 [cit. 2007-08-16]. Dostupné na webu: . [22] MAREK, Mizanin. DirectX [on-line]. [2002–2006] , 2006-01-01 [cit. 2007-08-16]. Dostupné na webu: . [23] Visual C++ Express – Easy to Use [on-line]. Microsoft Corporation, 1997 [cit. 200708-16]. Dostupné na webu: .
8.1 Použité zdroje
Prodáno 14.10.2014 na www.Kosmas.cz zákazníkovi [email protected], číslo objednávky 591522, UID: 7e15c933-e4b8-4e2a-94a6-c3fd2dffdec9
220
DIRECTX
8. Co dál?
Prodáno 14.10.2014 na www.Kosmas.cz zákazníkovi [email protected], číslo objednávky 591522, UID: 7e15c933-e4b8-4e2a-94a6-c3fd2dffdec9
DIRECTX
221
Rejstřík A
D
Acquire, členská metoda 185, 191 AddComponent, členská metoda 212 antialiasing 121 API 7 ARGB 149
DDCOLORKEY, struktura 69 DDSCAPS2, struktura 38, 41 DDSURFACEDESC2, struktura 38, 41, 56 DefWindowProc, funkce 23, 25 DeleteDC, funkce 57 DeleteObject, funkce 57 DIMOUSESTATE, struktura 189 DIMOUSESTATE2, struktura 190 DirectDraw 29
B barevná paleta 58 barevný klíč 62, 66 BeginScene, členská metoda 110 BltFast, členská metoda 46 BMP, grafický formát 57 buffer 30
C Clear, členská metoda 87, 89, 140 ClientToScreen, funkce 51 Close, členská metoda 215 CloseDown, členská metoda 204 CloseWindow, funkce 38, 86 CoCreateInstance, funkce 202, 209, 211, 215 CoInitialize, funkce 202, 209 COM 11 Connect, členská metoda 213, 216 CopyMemory, funkce 200 CoUninitialize, funkce 205 CreateCompatibleDC, funkce 56 CreateDevice, členská metoda 87, 88, 183, 190 CreateFile, funkce 61 CreateIndexBuffer, členská metoda 116 CreateOffscreenPlainSurface, členská metoda 98 CreatePalette, členská metoda 62 CreateSoundBuffer, členská metoda 198 CreateSurface, členská metoda 39, 41 CreateVertexBuffer, členská metoda 108, 109
back buffer 30 front buffer 30 integrace do systému 30 přehled rozhraní 31
DirectDrawCreateEx, funkce 38 DirectInput 179 DirectInput8Create, členská metoda 183 DirectMusic 195, 201 rozhraní 202
DirectPlay 207 rozhraní 209 sessions 208 zprávy 210
DirectSound 195–197 buffer 197
DirectSoundCreate8, funkce 198 DirectX 7–10 historie 13 kompatibilita 14 nastavení projektu 16 přehled 12 rozhraní 11 zjištění verze 25–27
DirectX SDK 10 DirectXSetupGetVersion, funkce 26–28 Direct3D 73 back buffer 76 front buffer 76 integrace do systému 74
Rejstřík
Prodáno 14.10.2014 na www.Kosmas.cz zákazníkovi [email protected], číslo objednávky 591522, UID: 7e15c933-e4b8-4e2a-94a6-c3fd2dffdec9
222
DIRECTX
pipeline 75 přehled rozhraní 76
Direct3DCreate9, funkce 87 DispatchMessage, funkce 19 double buffering 31 Download, členská metoda 203 drátový model 125 DrawIndexedPrimitive, členská metoda 118 DrawPrimitive, členská metoda 111 DrawSubset 130 DrawText, členská metoda 155 DrawText, funkce 42, 154 DSBUFFERDESC, struktura 199 D3DCAPS9, struktura 138 D3DCOLORVALUE, struktura 131 D3DDISPLAYMODE, struktura 91 D3DFORMAT, výčtový typ 91, 117 D3DLIGHT9, struktura 137 D3DMATERIAL9, struktura 131 D3DPRESENT_PARAMETERS, struktura 88, 97 D3DRECT, struktura 89 D3DX_PI, konstanta 123 D3DXCreateTextureFromFile, funkce 142 D3DXCreateTextureFromFileEx, funkce 148 D3DXIMAGE_INFO, struktura 99 D3DXLoadMeshFromX, členská metoda 126, 133 D3DXLoadSurfaceFromFile, funkce 98 D3DXMATRIX, struktura 119 D3DXMatrixMultiply, funkce 123 D3DXMatrixPerspectiveFovLH, funkce 122 D3DXMatrixRotationAxis, funkce 171 D3DXMatrixRotationX, funkce 121 D3DXMatrixRotationY, funkce 121 D3DXMatrixRotationZ, funkce 121 D3DXMatrixScaling, funkce 121 D3DXMatrixTranslation, funkce 121, 122 D3DXVECTOR3, struktura 122 D3DXVec3TransformCoord, funkce 171
E EnumAdapterModes, členská metoda 92 EnumHosts, členská metoda 212, 216 EnumServiceProviders, členská metoda 210
F Flip, členská metoda 42
G GetAdapterModeCount, členská metoda 91, 93 GetAttachedSurface, členská metoda 39, 41 GetBackBuffer, členská metoda 100, 163 GetClientRect, funkce 39, 41 GetCursorPos, funkce 192 GetDC, členská metoda 42, 57 GetDeviceCaps, členská metoda 75, 138 GetDeviceState, členská metoda 185, 192 GetHWnd, funkce 96 GetModuleHandle, funkce 23 GetObject, funkce 56 GetPixel, funkce 70 GetStockObject, funkce 23 GetSurfaceDesc, členská metoda 56 GetSystemMetrics, funkce 49 GetTickCount, funkce 35, 83 GetTransform, členská metoda 124
H handle 20 device context 42 instance 19 okna 22
Host, členská metoda 211, 215 hrana 77
I IDirectDraw, rozhraní 31 IDirectDrawClipper, rozhraní 31 IDirectDrawPalette, rozhraní 31, 60 IDirectDrawSurface, rozhraní 31 IDirectDrawSurface7, rozhraní 31, 37 IDirectDraw7, rozhraní 37 IDirectInputDevice8, rozhraní 180, 182 IDirectInput8, rozhraní 179, 182 IDirectMusicLoader8, rozhraní 202 IDirectMusicPerformance8, rozhraní 202 IDirectMusicSegment8, rozhraní 202 IDirectPlay8Address, rozhraní 211, 216 IDirectPlay8Client, rozhraní 216 IDirectPlay8Peer 209 IDirectPlay8Server, rozhraní 215 IDirectSoundBuffer8, rozhraní 197, 200 IDirectSoundFXEcho8, rozhraní 198
Rejstřík
Prodáno 14.10.2014 na www.Kosmas.cz zákazníkovi [email protected], číslo objednávky 591522, UID: 7e15c933-e4b8-4e2a-94a6-c3fd2dffdec9
DIRECTX
IDirectSoundFXChorus8, rozhraní 198 IDirectSound3DListener8, rozhraní 198 IDirectSound8, rozhraní 197 IDirect3DDevice9, rozhraní 76, 85 IDirect3DIndexBuffer9, rozhraní 76, 114 IDirect3DSurface9, rozhraní 76 IDirect3DTexture9, rozhraní 76, 142 IDirect3DVertexBuffer9, rozhraní 76, 105 IDirect3D9, rozhraní 76, 85 IDirect3D9Surface9, rozhraní 96 ID3DXFont, rozhraní 154 ID3DXMesh, rozhraní 125 index buffer 113 InitAudio, členská metoda 203 Initialize, členská metoda 210, 215
peer-to-peer 208 pixel shader 75 PlaySegment, členská metoda 204 PlaySound, funkce 195 plochy 77 POINT, struktura 51, 192 PostQuitMessage, funkce 23, 25 Present, členská metoda 87, 90, 101 programový projekt 16 průhlednost 62
R
kamera 119 klient/server 208, 215
ReadFile, funkce 61 RECT, struktura 36 Release, členská metoda 32, 80, 181 ReleaseDC, členská metoda 42, 57 Reset, členská metoda 102, 103 Restore, členská metoda 47, 200 RGB 42, 59, 92
L
S
LightEnable, členská metoda 138 LoadCursor, funkce 23 LoadIcon, funkce 23 LoadImage, funkce 56 LoadObjectFromFile, členská metoda 203 Lock, členská metoda 70, 109, 200 lstrcmp, funkce 95 lstrlen, funkce 163
SelectObject, funkce 56 Send, členská metoda 216 SendTo, členská metoda 214, 216 SetBkColor, funkce 42, 163 SetColorKey, členská metoda 69 SetCooperativeLevel, členská metoda 39, 183, 191, 198 SetDataFormat, členská metoda 183, 190 SetDisplayMode, členská metoda 39 SetFormat, členská metoda 199 SetFVF, členská metoda 111 SetIndices, členská metoda 118 SetLight, členská metoda 138 SetMaterial, členská metoda 132 SetPalette, členská metoda 60 SetPixel, funkce 70 SetRenderState, členská metoda 122, 128, 135, 139, 149, 164 SetSP, členská metoda 211 SetStreamSource, členská metoda 111 SetTextColor, funkce 42, 163 SetTexture, členská metoda 143 SetTextureStageState, členská metoda 143 SetTransform, členská metoda 122
K
M mciSendCommand, funkce 196 memcpy, funkce 109, 117 MessageBox, funkce 19, 26–28 mlha 164 MSG, struktura zprávy 19
O off-screen surface 51–53 OnLostDevice, členská metoda 155
P PALETTEENTRY, struktura 62, 99 PeekMessage, funkce 19
Rejstřík
Prodáno 14.10.2014 na www.Kosmas.cz zákazníkovi [email protected]
223
224
DIRECTX
ShowWindow, funkce 23, 24 single buffering 31 Sleep, funkce 103 sprintf, funkce 26–28, 32 sprite 62 stencil buffer 90 Stop, členská metoda 204 StretchBlt, funkce 56 StretchRect, členská metoda 100, 163 surfaces 29 světelné zdroje 135 světelný zdroj ambientní 136 bodový směrový 136 bodový všesměrový 136 plošný směrový 136
T TerminateSession, členská metoda 212 TestCooperativeLevel, členská metoda 100, 102 texty 154 TextOut, funkce 163 textury 140 průhlednost 147
transformace 78, 119, 152 TranslateMessage, funkce 19 triple buffering 31, 43
U událost 18 Unlock, členská metoda 70, 109, 200
UpdateWindow, funkce 23, 25 uv mapování 141
V va_end, funkce 158 va_start, funkce 158 vertex buffer 104 vertexy 77 _vstprintf, funkce 158
W WAVEFORMATEX, struktura 199 WinMain, funkce 19 Win32 aplikace 18 Win32 Application 16 WM_ACTIVATEAPP, zpráva 34, 82 WM_KEYDOWN, funkce 23 WM_KEYDOWN, zpráva 25, 167 WM_QUIT, zpráva 19, 20, 25 WNDCLASS, struktura 23 WndProc, funkce 22, 25
X x, grafický formát 124
Z z-buffer 90, 139 ZeroMemory, funkce 23 zpráva 18 ztracené surfaces 46
Rejstřík
Prodáno 14.10.2014 na www.Kosmas.cz zákazníkovi [email protected], číslo objednávky 591522, UID: 7e15c933-e4b8-4e2a-94a6-c3fd2dffdec9