Monthly archive for February, 2008

Nie zabijaj pikseli swoich nadaremno

2008-02-20 15:12

Począwszy od wersji 1.1, w pixel shaderach istnieje instrukcja texkill, której odpowiednikiem w HLSL jest funkcja clip. Działanie ich obu polega z grubsza na całkowitym odrzuceniu danego piksela, czyli nierysowaniu go. Shader wprawdzie nadal produkuje jakiś rezultat, ale nie jest on uwzględniany, a więc rzeczony piksel po prostu nie pojawia się na ekranie.

Biorąc pod uwagę to, że w kodzie pixel shadera możemy już całkiem sporo, można by uznać, że texkill to niezłe narzędzie do eliminowania niechcianych fragmentów sceny (przycinanych np. ustalonymi płaszczyznami). Lecz jak zawsze jest pewne ‘ale’, a nawet kilka. Oto one:

  • Wykonanie instrukcji texkill nie powoduje natychmiastowego zakończenia działania shadera dla danego piksela. Jak wiadomo, piksele (wierzchołki zresztą też) są przetwarzane na GPU równolegle i dlatego nie ma dla nich odpowiednika instrukcji ret(urn) ze zwykłych programów.
  • Piksel usunięty przez texkill może zepsuć anti-aliasing. Shader jest bowiem wywoływany tylko raz dla jednego piksela, zaś ponownie wygładzenie krawędzi wymagałoby modyfikacji koloru pikseli sąsiadujących z tym usuniętym – czyli ponownego uruchomienia dla nich pixel shadera.
  • Używanie instrukcji texkill uniemożliwia szybkie odrzucenie pikseli poprzez tzw. wczesny Z-Test, czyli test dokonywany przed uruchomieniem pixel shadera. Skoro bowiem piksel może zniknąć z innych powodów, to testowanie głębokości trzeba zostawić na później (chyba że wyłączony został zapis do Z bufora).

Nie znaczy to oczywiście, że texkill jest zły, bo w niektórych sytuacjach bywa nieoceniony. Przykładem jest choćby opisane przez Blinda i Rega rzucanie cieni przez obiekty z częściowo przezroczystymi teksturami. Ważne, aby z tego “triku” korzystać ze świadomością możliwych konsekwencji – wydajnościowych, rzecz jasna.

Tags: ,
Author: Xion, posted under Programming » Comments Off on Nie zabijaj pikseli swoich nadaremno

Błędne błędy

2008-02-19 19:47

Przeglądając ostatnio kod swojego… nazwijmy to umownie: silnika (;P), zauważyłem pewne dość ciekawe rozwiązanie. Jednak w tym przypadku ‘ciekawe’ znaczy mniej więcej tyle, co ‘zastanawiające’, ‘wątpliwe’ i ‘cokolwiek dziwne’. Chodzi o obsługę przeróżnych błędów czasu wykonania – czyli wyjątków – a w szczególności o ich rodzaje, wyróżnione w postaci klas obiektów reprezentujących rzeczone błędy.
Nie wiedzieć czemu podzieliłem je bowiem ze względu na źródło błędów. Zaowocowało to klasami wyjątków pochodzących od Windows API, od DirectX czy wreszcie takich, których źródło tkwiło w samym kodzie silnika. Oczywiście taki obiekt wyjątku miał też jakieś dodatkowe dane, co w najprostszej wersji ograniczało się po prostu do tekstowego komunikatu o tym, co się stało.

Nie waham się stwierdzić, że taka organizacja klas wyjątków to pomysł po prostu poroniony :) Na etapie obsługi błędów o wiele bardziej interesuje nas bowiem ich przyczyna, która powinna być określona jak najdokładniej. Stwierdzenie, że mamy do czynienia np. z błędną wartością argumentu naszej funkcji, mówi o wiele więcej niż fakt, że wartość ta spowodowała dalej błąd przy korzystaniu z jakiejś zewnętrznej biblioteki – na przykład DirectX. Różnica polega chociażby na tym, że dysponując klasami wyjątków w rodzaju InvalidArgumentException zamiast WindowsAPIError możemy wcześniej sygnalizować nieprawidłowe sytuacje. Ich obsługa jest też wygodniejsza, gdyż duża liczba klas wyjątków ułatwia oddzielanie od siebie fragmentów odpowiedzialnych za radzenie sobie z różnymi typami błędów.

Morał jest więc prosty: ważniejsze jest to, co konkretnie poszło źle – nie zaś to, gdzie rzecz się stała. Świadczą o tym także wyjątki spotykane w bibliotekach pokroju STL, .NET czy JDK. Wszędzie tam powyższa zasada jest nadrzędna.

Tags: ,
Author: Xion, posted under Programming » 1 comment

Tekstury do wszystkiego

2008-02-17 21:10

W potocznym rozumieniu tekstura to taki obrazek, który nakłada się obiekt trójwymiarowy, aby w ten sposób imitować wygląd jego powierzchni. Rzeczywiście, dawno temu była to ich jedyna funkcja. Tego rodzaju tekstury (nazywane teksturami rozproszenia) są oczywiście nadal niezbędne. Obok nich powstało jednak całe mnóstwo innych rodzajów tekstur, które są wykorzystywane podczas renderowania scen 3D.

Wśród nich są na przykład takie, które zawierają pewne niezbędne informacje na temat obiektów na scenie – nie tylko zresztą geometrii. Są to chociażby:

  • Przykład mapy wysokości
    Przykład mapy wysokości

    Mapy wysokości (height maps). To czarno-białe tekstury, używane do modelowania terenu. Jasność konkretnego piksela odpowiada wysokości terenu w danym punkcie. Taka tekstura musi być naturalnie przetworzona na odpowiednie trójkąty (co specjalnie trudne nie jest), ale jej używanie zamiast innych reprezentacji ma dwie wyraźne zalety. Po pierwsze, umożliwia regulowanie stopnia szczegółowości (Level of Detail, LoD) wyświetlanego terenu. Po drugie, mapy wysokości są bardzo łatwe do wykonania za pomocą dowolnego programu graficznego nawet przez średnio uzdolnionego w tym kierunku kodera :)

  • Przykład mapy normalnych
    Odpowiadająca jej
    mapa normalnych

    Mapy normalnych (normal maps) obrazują z kolei wektory normalne punktów powierzchni. Pomysł jest bardzo prosty: kolor każdego piksela w formacie RGB odpowiada normalnej o współrzędnych XYZ. Ponieważ współrzędne te są ograniczone (długość normalnej to zawsze 1), mogą być zapisane jako kolor. Mapa normalnych jest potem wykorzystywana przy obliczeniu oświetlenia per-pixel.

  • Mapy światła (light maps) reprezentują natomiast rozkład oświetlenia na scenie lub wokół źródła światła. W tym pierwszym przypadku chodzi o użycie wygenerowanego wcześniej jakąś kosztowną metodą (np. śledzenia fotonów) “wzorca” oświetlenia sceny. Ma to rzecz jasna sens tylko wtedy, gdy oświetlenie jest statyczne. Z kolei lightmapa dla źródła światła obrazuje kształt promieni świetlnych, które rzekomo z niego wychodzą. W obu przypadkach jasność pikseli odpowiada intensywności oświetlenia, chociaż mapy światła nie muszą być monochromatyczne.

Tego rodzaju tekstury są przygotowywane wcześniej i obok modeli, tekstur rozproszenia i innych danych stanowią informacje umożliwiają renderowanie sceny. Oprócz nich w trakcie samego rysowania wykorzystywane są też inne tekstury, tworzone na bieżąco i zwykle niezachowywane na później. Wśród tych efemerycznych tekstur mamy na przykład:

  • Mapy cieni (shadow maps). Są one używane przy jednej z technik liczenia cieni. Pojedyncza mapa to po prostu zapis bufora głębokości dla sceny widzianej z punktu widzenia źródła światła. Dzięki temu możliwe jest następnie określenie, który piksel jest widoczny dla tegoż źródła, a który jest w cieniu. To dość prosty sposób obliczenia cieniowania, w podstawowej wersji wymaga jednak dodatkowego przebiegu dla każdego źródła światła.
  • Imbryk z mapą sześcienną
    Imbryczek z cubemapą

    Mapy odbić środowiskowych (environmental maps) służą do symulowania przedmiotów o powierzchniach lustrzanych. Podobnie jak wyżej, wymagają osobnego przebiegu renderowania, i to często nawet niejednego (jak w przypadku map sześciennych). Tak powstałe obrazy odbić są potem nakładane na przedmiot, który dzięki temu sprawia wrażenie, jakby odbijał w sobie resztę sceny.

  • Bufory geometrii (G-buffers) to w zasadzie nie jedna, a zestaw tekstur, z których każda zawiera informacje o pewnym parametrze materiału dla danego piksela z gotowego obrazu sceny. Po wypełnieniu ich informacjami, “bufor” ten jest wykorzystywany np. dla obliczeń związanych z oświetleniem i cieniowaniem we wszystkich technikach opatrzonych modnym przydomkiem ‘deferred‘. Dzięki temu oszczędza się każdorazowego przekształcania całej geometrii dla każdego przebiegu renderowania przez wszystkie macierze.

Potencjalnych i aktualnych zastosowań tekstur jest o wiele więcej. Ale już na tych przykładach widać, że tekstury tak naprawdę nie są obrazkami, a jedynie zbiorami jakichś informacji, które tylko z konieczności są zapisywane w postaci kolorów pikseli. Być może niedługo staną się one pełnoprawną “pamięcią operacyjną” kart graficznych, którą można będzie np. alokować i zwalniać w kodzie shaderów. Jak dotąd możliwy jest ich odczyt oraz w pewnym stopniu zapis (zależnie od modelu shaderów), ale kto wie – może wkrótce doczekamy się czegoś więcej?…

Podsumowanie magicznej siódemki

2008-02-16 14:02

Pozwolę sobie poczynić zupełnie niekoderski wpis, jako że od ostatniego podobnego minęło już dobrych kilka miesięcy. Myślę, że mogę od czasu do czasu pozwolić sobie na coś takiego :) Zwłaszcza, że mam ku temu pewną dość szczególną okazję…

Okładka książki “Harry Potter i Insygnia Śmierci”Dotarłem właśnie do końca niezbyt może ambitnego, za to niezwykle popularnego kawałka literatury. Mówię tu oczywiście o serii książek pod tytułem Harry Potter i jeszcze coś, której to (prawdopodobnie) ostatni tom niedawno trafił na półki polskich księgarń. Jak nietrudno się domyślić, mam nieodpartą ochotę, aby podzielić się wrażeniami z lektury ;P
I muszę przyznać, że jestem nią w pełni usatysfakcjonowany. Opowieści o przygodach Pottera już od jakiegoś czasu nie były bajkami dla dzieci, lecz chyba dopiero w tym ostatnim tomie ujawniło się, iż mamy tutaj do czynienia z całkiem dojrzałą pozycją z gatunku fantasy. Pod względem fabularnym też nie można jej wiele zarzucić, chociaż wiadomo, że dla wszystkich najważniejsze były suche fakty: jak to się skończy, kto zwycięży, kto przeżyje?… Tym niemniej cieszy to, że ostatecznie kilka spraw wyjaśnia się na sposób daleki od przewidywań: ci, którzy wydawali się być charakterami bezwarunkowo czarnymi, nie zawsze takimi się okazują, a na kryształowych obliczach postaci wyjątkowo “czystych” pojawiają się wyraźne rysy.

Koniec sagi o młodym czarodzieju to pewnie najlepsza okazja, aby zastanowić się, dlaczego historie te stały się aż tak popularne. Bo chyba stwierdzenie, że obecnie przy sprawnym marketingu można wypromować cokolwiek, nie będzie w pełni zadowalające (miejmy przynajmniej taką nadzieję). Ten produkt musi mieć po prostu takie obiektywne coś, co zadecydowało o jego sukcesie. Według mnie lista tych “cosiów” zawiera przynajmniej trzy pozycje:

  • Dopracowany i bogaty w szczegóły świat. Książki o Potterze są osadzone w świetnie zaprojektowanych, fikcyjnych realiach. Może nie mają one rozmachu tolkienowskiego Śródziemia, ale bezsprzecznie działają na wyobraźnię. Dodatkowo świetnie sprawdzają się jako miejsce akcji dla historii dowolnego typu, od przygodowych po obyczajowe.
  • Historia oparta na sprawdzonym niezliczoną ilość razy motywie. Oto bohater, który nagle znajduje się w zupełnie nieznanych dla siebie okolicznościach i musi przy tym spełnić ogromnie trudną i niezwykle ważną misję, od której zależy bardzo, bardzo wiele. Seria Star Wars, Matrix, Władca Pierścieni, większość opowieści o superbohaterach, a nawet część starożytnych mitów i chrześcijańskie ewangelie – wszystkie opierają na wariantach tego pomysłu. Nie inaczej jest z Harrym Potterem, który z każdym kolejnym tomem coraz bardziej czuje na sobie ciężar nieuchronnego przeznaczenia.
  • Dopasowanie stylu do grupy docelowej. Wyjątkową cechą książek o Potterze jest to, że zdają się dorastać wraz ze swoim czytelnikiem. O ile pierwsze części są prostymi i efektownymi historyjkami dla dzieci, o tyle dalej rozwijają się w znacznie bardziej rozwinięte (także objętościowo) powieści. Pomijając potwornie nudny i okropnie infantylny tom szósty, są one też świetnie skrojone pod względem fabularnym i stylistycznym. Na początku dominują w nich dialogi i wartka akcja, lecz potem (czyli po odpowiednim wyrobieniu czytelnika) autorka pozwala sobie – i słusznie – na dłuższe popisy narracyjne.

Czy to prosty przepis na sukces? Bynajmniej. Połączenie tych wszystkich elementów w spójną i atrakcyjną całość to z pewnością wielka sztuka.
Najciekawsze jest jednak to, że powyższą receptę można wcale nieźle dopasować do… wytworów działalności koderskiej. W końcu tu też liczy się dobry projekt, korzystanie ze sprawdzonych rozwiązań oraz dobra treść i forma. Efekty może nie są aż tak “magiczne”, ale niekiedy udaje się nimi zachwycić nawet mało obeznanych z tematem mugoli ;-)

Tags:
Author: Xion, posted under Books, Thoughts » 11 comments

Przesłanianie kontra nadpisywanie

2008-02-14 19:08

Jednym ze swego rodzaju kruczków programowania obiektowego jest różnica między przesłanianiem (hide) a nadpisywaniem (override) metod w klasach pochodnych. Niby nic w tym trudnego, dopóki nie uświadomimy sobie, że metody wirtualne – w przeciwieństwie do zwykłych – mogą podlegać obu tym procesom i że w ich przypadku różnią się one w dość istotny sposób.

Jeśli mianowicie mamy jakąś metodę wirtualną w klasie bazowej, to w klasie pochodnej teoretycznie możemy ją zarówno nadpisać, jak i przesłonić. Gdy zdecydujemy się na to pierwsze wyjście, wówczas nasz obiekt staje się polimorficzny. Posługując się wskaźnikiem lub referencją do obiektu klasy bazowej tak naprawdę możemy operować na obiekcie klasy pochodnej, a wywołując metodę wirtualną nie wiemy, która jej wersja będzie użyta. Wiemy jedynie, że będzie to ta “właściwa” (z klasy, do której należy nasz obiekt). No cóż, to podstawa OOP-u, jakkolwiekby na to nie patrzeć :)

  1. // klasa bazowa
  2. class CBase { public: virtual void Foo() { /* ... */ } };
  3.  
  4. // klasa pochodna
  5. class CDerived : public CFoo
  6. {
  7.    public:
  8.       // nadpisana metoda
  9.       void Foo()   { /* ... */ }
  10. };

Nie zawsze jednak musi tak być. Jeżeli metodę wirtualną się przesłoni, wtedy nic takiego nie zadziała i zostanie wywołana wersja z klasy bazowej. Typ obiektu nie zostanie bowiem rozpoznany w trakcie działania programu, a całe wywołanie będzie zapisane “na sztywno” w wynikowym kodzie. W gruncie rzeczy będzie tak samo, jakbyśmy przesłoni dowolną inną (niewirtualną) metodę.
O ile oczywiście możemy, bowiem w C++ metody wirtualnej przesłonić się po prostu nie da. Nadpisywanie jest po prostu wykonywane automatycznie i nie można z niego zrezygnować. W innych językach (jak w Delphi i C#) wymaga ono słowa kluczowego override. Tam też metody wirtualne można przesłaniać, stosując modyfikatory – odpowiednio reintroduce i new.

Żeby było zabawniej, przesłanianie i nadpisywanie może występować dla tej samej metody w tej samej hierarchii dziedziczenia. Metoda wirtualna zdefiniowana w klasie A może być na przykład nadpisywana w kolejnych klasach pochodnych A1, A2, A3, itd. I nagle w klasie B zostanie ona przesłonięta. Ba, ta przesłonięta wersja sama może być wirtualna i nadpisywana w dalszych klasach pochodnych: B1, B2, B3, …
Nie spotyka się tego często, jako że podobne wielopiętrowe drzewa dziedziczenia są rzadkie (i takie być powinny). Warto jednak o tym wiedzieć, bo nigdy nie wiadomo, kiedy podobny kruczek przybierze w naszym kodzie postać wielkiego kruka i zacznie nas dziobać zagadkowymi błędami ;]

Tags: , ,
Author: Xion, posted under Programming » 1 comment

Nie całkiem brakujące ogniwo DirectX

2008-02-12 20:49

Wszyscy wiedzą, że przesiadka na Vistę ma jedną niezaprzeczalną zaletę. Jeśli mianowicie dysponujemy odpowiednią kartą graficzną, to możemy cieszyć się całym bogactwem Shader Model 4 oraz dziesiątej wersji DirectX. Nadal jednak niewiele gier posiada jakieś efekty (oczywiście w(y)łączalne) uzyskiwane przy pomocy nowej wersji biblioteki. Najwyraźniej Microsoft nieumyślnie wpadł tutaj w rodzaj błędnego koła, które zapobiega powszechnemu wykorzystaniu DirectX 10.
Nie pomaga tu też za bardzo specjalna wersja “dziewiątki”, czyli tak zwany Direct3D 9Ex. Cóż to za zwierz?…

Jest to mianowicie pewne rozszerzenie znanej i lubianej wersji biblioteki, które wprowadza nowe możliwości, częściowo zbieżne z tym, czego można doświadczyć w D3D10. Wśród nich mamy chociażby:

  • Praktyczny brak zjawiska utraty urządzenia. W zasadzie począwszy od DirectDraw, gdzie mieliśmy tzw. utratę powierzchni, była to zawsze spora niedogodność. Jak wiemy, utrata urządzenia DirectX dotąd następowała wtedy, gdy użytkownik przełączał się z aplikacji pełnoekranowej, zmieniając tryb graficzny (rozdzielczość, głębię kolorów, itd.). Powodowała ona stratę wszystkich zasobów przechowywanych w pamięci karty graficznej, czyli głównie tekstur, i konieczność ich ponownego ładowania – co było i jest dość kłopotliwe. W D3D9Ex utrata urządzenia następuje tylko w dwóch ekstremalnych sytuacjach sprzętowo-sterownikowych i właściwie nie trzeba się nią przejmować.
  • Współdzielenie zasobów. Funkcje DirectX 9 miały od dawna tajemniczy parametr pSharedHandle. Był on opisany jako zarezerwowany i należało w jego miejsce przekazywać NULL. Rezerwacja ta jednak nie przepadła i w 9Ex jest on wykorzystywany do współdzielenia zasobów między urządzeniami – nawet takimi, które są w oddzielnych procesach. Mogę sobie wyobrazić zastosowanie tego mechanizmu chociażby do przełączania trybów graficznych bez konieczności ponownego tworzenia wszystkich tekstur, buforów, itp.
  • Przydatne funkcje w nowym interfejsie IDirect3DDevice9Ex. Jest wśród nich na przykład WaitForVBlank, służąca do ręcznej synchronizacji pionowej.

Wszystkie te możliwości prezentują się całkiem nieźle. Żeby jednak z nich korzystać, muszą być spełnione dwa warunki. Po pierwsze, sprzęt musi wspierać tak zwany WDDM (Windows Device Driver Model), co w przypadku popularnych kart graficznych dotyczy z grubsza tych, które udostępniają Shader Model 3.
A drugi warunek?… Niejako wynika on z pierwszego, bowiem WDDM jest częścią systemu Windows Vista. Aplikacje wykorzystujące Direct3D 9Ex będą więc działały wyłącznie na tym systemie. To pewnie trochę zniechęcające, prawda? Ale cóż, Microsoft próbuje jak może ;-)

Dziwne dokumenty RFC

2008-02-11 14:23

Pod odrobinę nieadekwatną nazwą RFC (Request For Comments, czyli prośba o komentarze) kryje się zbiór standardów czy raczej zaleceń dotyczących prawie każdego aspektu funkcjonowania Internetu. Opisy większości ważnych protokołów sieciowych – takich jak HTTP, FTP, POP3, SMTP, itd. – są zawarte właśnie w dokumentach RFC. Każdy taki dokument ma swój unikalny numer, który po publikacji nie ulega zmianie – podobnie zresztą jak sam dokument.
Brzmi to całkiem poważnie, prawda? W istocie, część dokumentów RFC ma kluczowe znaczenie dla działania globalnej sieci (jak choćby RFC2616, opisujący protokół HTTP). Wśród tysięcy już opublikowanych zdarzają się jednak i takie, które ze zwyczajowym technicznym przynudzaniem nie mają zbyt wiele wspólnego. Oto kilka takich przykładów:Roznosiciel pakietów IP

  • RFC1149 jest z tej grupy chyba najbardziej znanym. Opisuje on zalecany sposób przesyłania pakietów IP przy pomocy… pocztowych ptaków. Jak możemy wyczytać w dokumencie, ta metoda zapewnia detekcję kolizji w transmisji bez żadnych dodatkowych wysiłków, a dodatkowo tak zorganizowana komunikacja bezprzewodowa ma duży zasięg i nie podlega ograniczeniom związanym z topologią terenu. Trzeba jednak zaznaczyć, że “(…) opóźnienia są duże, przepustowość niewielka (…)”, zaś “(…) burze mogą powodować utratę danych” :-)
  • Innym ciekawym pomysłem jest RFC1437, który opisuje nowy typ zawartości MIME, mianowicie matter-transport/sentient-life-form. Jako że jest on przeznaczony do kodowania i przesyłania na odległości materii (nazwa wskazuje, że dotyczy to zwłaszcza organizmów żywych), będzie z pewnością bardzo użyteczny dla celów teleportacji :)
  • Kolejnym ciekawym sposobem na przesyłanie pakietów jest ten opisany w RFC1926. Każdemu ciągowi 4 bitów przyporządkowano mianowicie jedną głoskę. Dzięki temu możliwy jest przesył przy pomocy jakiegokolwiek medium oferującego transmisję głosu. Osobiście sądzę, że najefektywniejsze będą dwa kubki połączone sznurkiem lub rządek osób szepczących sobie do uszu ;]
  • RFC2324 oferuje z kolei bardzo interesującą propozycję protokołu o nazwie HTCPCP, co stanowi skrót od Hyper Text Coffee Pot Control Protocol. Służyć on ma do zdalnego kontrolowania ekspresów i automatów do kawy, aby możliwe było zapewnienie sobie świeżej porcji pożytecznego napoju niezależnie od naszego aktualnego położenia. Warto dodać, że w przyszłych wersjach standardu zostanie prawdopodobnie dodana obsługa espresso :D
  • RFC2550 sygnalizuje natomiast niezwykle palący problem roku 10.000, znany szerzej jako Y10K, który zmusi do poprawek w aktualnym oprogramowaniu, beztrosko korzystającym z faktu, iż rok ma zawsze cztery cyfry. Dokument wskazuje między innymi na krótkowzroczność pomysłu polegającego po prostu na dodaniu kolejnej cyfry do reprezentacji lat, który spowoduje przecież podobny problem już w roku 100.000. Zamiast tego wysuwana jest propozycja bardziej stabilnego rozwiązania, które może zacząć sprawiać kłopoty dopiero około roku 1038.
  • Wreszcie, w RFC3092 możemy przeczytać dokładną definicję terminu foo, tak beztrosko używanego w innych dokumentach RFC i nie tylko (a także jego nieco rzadziej spotykanego wariantu, czyli bar).

Patrząc na tę listę, można stwierdzić, że wiele bardzo ciekawych pomysłów z niewiadomych przyczyn nie doczekało się realizacji. Nie wiem na przykład, dlaczego porzucono wykorzystywanie sprawdzonego sposobu komunikacji z wykorzystaniem ptaków na rzecz jakichś wydumanych i zupełnie nieprzemawiających do wyobraźni światłowodów ;-D

Tags: , ,
Author: Xion, posted under Internet » 2 comments
 


© 2023 Karol Kuczmarski "Xion". Layout by Urszulka. Powered by WordPress with QuickLaTeX.com.