Prowadzący zajęcia projektowe: Rafał Petryniak (strona domowa zajęć projektowych).
Spis treści
Raytracing jest jedną z licznych technik, które istnieją, by zrobić obrazy z komputerami. Idea Raytracingu jest taka, że fizycznie poprawne obrazy są skomponowane przez światło i to światło zwykle pochodzi ze źródła światła i odbija sie dookoła jako promienie po scenie zanim dotrą do naszych oczu lub aparatu fotograficznego. Będąc zdolnym odtworzyć w symulacji komputerowej ścieżkę podążając ze źródła światła do naszego oka wtedy bylibyśmy w stanie określić to co widzi nasze oko . Oczywiście nie jest to tak proste jak brzmi. Potrzebujemy jakiejś metody, by śledzić te promienie, ponieważ natura ma nieskończoną ilość obliczeń dostępnych ale my nie mamy.
Jedną z najbardziej naturalnych idei raytracingu jest to , że my zajmujemy się tylko promieniami, które trafiły bezpośrednio do naszych oczu bezpośrednio lub po kilku odbiciach .
Druga metoda polega na tym, aby nasze generowane obrazy były siatkami pikseli z ograniczoną rozdzielczością.
Te dwa sposoby razem tworzą najbardziej podstawowe raytracery. Umieścimy nasz punkt widzenia w scenie 3D i wypuśćmy promienie wyłącznie z punktu widzenia w kierunku reprezentacji naszej siatki 2D w przestrzeni. Wtedy spróbujemy ocenić ilość odbić potrzebnych aby dojść z źródła światła do naszego oka. To jest w porządku, ponieważ aktualna symulacja światła aby być dokładna nie bierze pod uwagę faktycznego kierunku promienia, . Oczywiście to jest uproszczenie będziemy widzieć później dlaczego.
Idea kryjąca się za raytracingiem jest prosta na tyle że moglibyśmy jej użyć wszędzie. Jednak nie wszędzie jest używana. Raytracing jest używany od kilku dekad w środowisku filmowym jako renderer offline. Jest to rendering w którym nie jest wymagane ukończenie całej sceny w kilka milisekund. Oczywiście nie można generalizować i trzeba zwrócić uwagę na to że istnieje kilka implementacji raytracingu pozwalających na poczucie interaktywności. Nazywamy to raytracingiem w czasie rzeczywistym. Jest to aktualnie jedna z rzeczy która napędza rozwój współczesnych akceleratorów 3D. Raytracerów używamy najczęściej tam gdzie ważna jest jakość odbić od materiałów. Sporo efektów, trudnych do uzyskania za pomocą innych metod renderowania wygląda bardzo naturalnie po użyciu raytracingu. Odbicia, załamania, głębia ostrości, wysokiej jakości cienie. Karty graficzne potrafią generować wiele typów obrazu ale sa bardzo ograniczone pod względem raytracingu. Nikt nie jest w stanie powiedzieć czy ograniczenie to zostanie usunięte w przyszłości.
Alternatywą do raytracingu używaną przez karty graficzne jest rasteryzacja. Rasteryzacja ma zupełnie inne podejscie do renderingu. Jej podstawą są trójkąty a nie promienie. Dla każdego trójkąta na scenie można oszacować zajmowaną przez niego powierzchnię ekranu i później dla każdego widzialnego piksela tego trójkąta można obliczyć jego kolor. Karty graficzne bardzo dobrze sobie radzą z rasteryzacją ponieważ są w stanie wykonać wiele optymalizacji z nią związanych. Rasteryzacja bierze pod uwagę tylko tą geometrie sceny którą widzimy. Raytracing natomiast w czasie obliczeń ma na uwadze geometrie całej sceny. Możemy powiedzieć że raytracing jest globalny, a rasteryzacja lokalna. Poza prostymi decyzjami nie ma żadnych rozgałęzień w rasteryzacji. Są one jednak nieodłączną częścią raytracingu.
Raytracing nie jest też używany wszędzie w renderingu offline. Szybkość rasteryzacji i innych technik jest często stawiana naprzeciw prawdziwemy raytracingowi, szczególnie dla tzw promieni głównych. Promienie główne to te które trafiają bezpośrednio do naszego oka bez odbić. Promienie te są spójne i mogą być przyspieszone projektywną matematyką (za pomocą interpolacji od której są zależne szybkie rasteryzatory). Dla promieni pobocznych (odbitych od powierzchni, załamanych) wszystko zostaje wyłączone ponieważ ich właściwości zanikają. Należy zwrócić uwagę na to, że obliczenia raytracingu mogą być potem użyte do wygenerowania danych potrzebnych w rasteryzacji. Symulacja zwana Global illumination potrzebuje czasem wypuścić promień aby zbadać lokalne właściwości materiału. Obliczenia te są potem używane w całej scenie przyspieszając zwykły renderer.
Raytracer nie jest i nie będzie kompletnym rozwiązaniem. Nie ma istniejącego rozwiązania potrafiącego sobie poradzić z prawdziwą losową nieskończonością. Często wyzwaniem jest skoncentrowanie się na tym co czyni obraz i jego wygląd bardziej realistycznym. Nawet dla rendererów offline ważna jest też wydajność. Trudno podjąć decyzje o wypuszczeniu bilionów promieni na miliony pixeli, nawet jeżeli wydaje się że symulacja tego wymaga. Musimy zdecydować się nie podążać za każdą dostępną wiązką promieni. Globalna iluminacja jest tego dobrym przykładem. W jej technikach takich jak mapowanie fotonów próbujemy istalić najkrótszą ścieżkę pomiędzy światłem a powierzchnią na której się znajdujemy. Generalnie, wykonanie pełnej globalnej iluminacji polega na wypuszczeniu nieskończonej liczby promieni w każdym kierunku i obserwowaniu jaki ich procent trafi w światło. Możemy tak zrobić jednak jest to bardzo wolne. Wypuszczamy fotony (używając takiego samego algorytmu jak w raytracingu jednak w przeciwną strone, od źródła światła) i patrzymy w jakie powierzchnie uderzają. Następnie używamy tych informacji aby obliczyć oświetlenie w jednej aproksymacji dla każdego punktu powierzchni. Śledzimy nowy promień tylko wtedy jeżeli możemy uzyskać dobry efekt (perfekcyjne odbicie wymaga tylko jednego dodatkowego promienia). Nawet wtedy może to być obciążające jako że drzewo promieni rozrasta sie w sposób geometryczny. Musimy zatem ograniczać maksymalny stopień szczegółowości.
Nawet wtedy chcemy zrobić to szybciej. Wykonywanie testów przecięć staje sie zwężeniem jeżeli mamy miliony przecinających się obiektów na scenie i zaczynamy liniowo poszukiwać przecięć dla każdego z promieni. Wtedy potrzebujemy jakiejś struktury przyspieszającej. Najczęściej używaną jest struktura hierarchiczna sceny. Przychodzi na myśl coś w stylu drzewa. Struktura ta jest zależna od typu sceny, od połączeń jakie muszą zostać zaktualizowane w przypadku ruchomych obiektów, jak i od pamięci jaką możemy przydzielić. Struktury przyspieszające mogą też przechowywać fotony i wiele innych rzeczy jak zapis fizycznych kolizji. Generalnie w celu przyspieszenia należałoby używać rasteryzacji dla promieni głównych i raytracingu dla promieni pobocznych. Wymaga to użycia sprzętu który radzi sobie dobrze w obu kwestiach.
1. Z punktu widzenia obserwatora wyprowadzamy promień przecinający płaszczyznę na której powstanie obraz. Promień ten nazywamy pierwotnym.
2. Wyszukiwany jest najbliższy punkt przecięcia z obiektami na scenie
3. Dla każdego punktu sprawdzamy czy jest on przesłonięty przez inny obiekt i znajduje się w cieniu. Jeśli nie to dla każdego źródła światła wyznaczana jest wartość jasności w tym punkcie
4. Dla obiektów odbijających światło następuje przetwarzanie rekurencyjne tzn. z badanego wcześniej punktu wysyłane są promienie wtórne. Wracamy do kroku 2. Dopiero po kilkukrotnym obliczeniu zwracana jest wartość jasności pixela.
Schemat ten jest powtarzany kilkukrotnie dla każdego pixela w celu uniknięcia aliasingu.
Nasz raytracer nie jest czasu rzeczywistego dlatego nie skupiliśmy się tutaj głównie na optymalizacji tylko na pokazaniu koncepcji działania tak aby każdy kawałek kodu był jak najbardziej zrozumiały.
Program na wejście wczytuje plik sceny taki jak ten:
////////////////////////////////////// // Scena globalna i punkt widzenia // /////////////////////////////////////// Scene { Version.Major = 1; Version.Minor = 0; // rozmiar_wyjsciowy Image.Width = 640; Image.Height = 480; // Ilosc materialu w scenie // description will follow NumberOfMaterials = 3; NumberOfSpheres = 3; NumberOfLights = 2; } /////////////////////////////////////// // Lista materialow // /////////////////////////////////////// Material0 { Diffuse = 1.0, 1.0, 0.0; Reflection = 0.5; } Material1 { Diffuse = 0.0, 1.0, 1.0; Reflection = 0.5; } Material2 { Diffuse = 1.0, 0.0, 1.0; Reflection = 0.5; } /////////////////////////////////////// // lista sfer // /////////////////////////////////////// Sphere0 { Center = 233.0, 290.0, 0.0; Size = 100.0; Material.Id = 0; } Sphere1 { Center = 407.0, 290.0, 0.0; Size = 100.0; Material.Id = 1; } Sphere2 { Center = 320.0, 140.0, 0.0; Size = 100.0; Material.Id = 2; } /////////////////////////////////////// // Lista swiatel // /////////////////////////////////////// Light0 { Position = 0.0, 240.0, -100.0; Intensity = 1.0, 1.0, 1.0 ; } Light1 { Position = 640.0, 240.0, -10000.0; Intensity = 0.6, 0.7, 1.0; }
Na wyjściu dostajemy obraz:
dla każdego piksela na ekaranie { Końcowy kolor = 0; Promień = { punkt startowy, kierunek }; Powtóż { dla kazdego obiektu w scenie { wyznacz najblizszy promień obiekt/przecięcie; } Jezeli przecięcie istnieje { dla kazdego światła w scenie { Jeżeli światło nie jest w cieniu innego obiektu { to dodaj udział światła do policzonego koloru; } } } Końcowy kolor = Końcowy kolor + policzony kolor * poprzedni współczynnik odbicia; współczynnik odbicia = współczynnik odbicia * właściwości odbicia powierzchni; zwiększenie głębi; } wykonaj dopuki współczynnik odbicia jest 0 albo osiagnięta jest maksymalna głebia; }
Obiektami są sfery, jako źródło światła przyjmujemy punkt.
Wysyłamy promień w każdy piksel aby określić jego kolor:
for (int y = 0, y <myScene.sizey; + + y) ( for (int x = 0; x <myScene.sizex; + + x) ( / / ... ) )
Po obliczeniu koloru zapisujemy go do pliku TGA
imageFile.put (min (blue * 255.0f, 255.0f)). put (min (green * 255.0f, 255.0f)). put (min (red * 255.0f, 255.0f));
Nasze kolory są reprezentowane jako trzy liczby zmiennoprzecinkowe.
Punkt startowy znajduje się gdzieś na naszej scenie projekcji. Promienie są skierowane w jednym kierunku (w stronę dodatniej osi Z)
ray viewRay = ((float (x), float (y),-1000.0f), (0.0f, 0.0f, 1.0f));
Jeden kierunek jest ważny ponieważ określa naszą projekcje. W naszym przypadku użyjemy projekcji prostopadłej (perpendicular projection lub orthographic projection). Typ projekcji określa w jaki sposób promienie są powiązane ze sobą. Jednak nie zmienia to zachowania pojedynczego promienia.
Promień jest zdefiniowany jako punkt startowy i kierunek. Parametr 't' jest to odległość dowolnego punktu promienia do punktu startowego promienia.
Przesuwamy tylko w jednym kierunku (z mniejszego t do większego t). Będziemy iterować przez każdy obiekt, sprawdzając czy istnieje punkt przecięcia, znajdziemy parametr t odpowiadający przecięciu, bieżemy najbliższy i jednocześnie najmniejszy jaki jest możliwy.
Obiektami są sfery więc będziemy mieli tylko takie przecięcia. Są to jedne z najprostszych przecięć jakie istnieją.
Wprowadzamy nasz parametr do równania przecięcia i daje nam to równanie drugiego stopnia z niewiadomą t.
Obliczamy Δ, jeśli Δ < 0 to nie ma przecięć, jeśli Δ >0 to mamy co najwyżej dwa przecięcia, bierzemy to najbliższe.
vecteur dist = s.pos - r.start; double B = r.dir * dist; double D = B*B - dist * dist + s.size * s.size; if (D < 0.0f) return false; double t0 = B - sqrt(D); double t1 = B + sqrt(D); bool retvalue = false; if ((t0 > 0.1f) && (t0 < t)) { t = (float)t0; retvalue = true; } if ((t1 > 0.1f) && (t1 < t)) { t = (float)t1; retvalue = true; }
Bierzemy najbliższe, ale nie może ono być w tym samym położeniu co punkt startowy, dlatego porównujemy t0 i t1 do 0.1f.
Oświetlenie w jednym punkcie jest równe sumie składowych poszczególnych źródła światła. Jest to bardzo uproszczone dzięki zapewnieniu listy zdefiniowanych punktów światła.
Nie bierzemy światła, które nie jest widoczne z naszej strony powierzchni. Tak jest jeśli wychodząca normalna do naszej sfery i kierunek światła są w przeciwnym kierunku:
// znak po wkonaniu działania pokazuje czy są w przeciwnym kierunku if ( n * dist <= 0.0f ) continue;
Następnie sprawdzamy czy punkt jest w cieniu innego obiektu.
Powierzchnia ma współczynnik odbicia. Jeżeli wartość tego współczynnika wynosi 0, wówczas powierzchnia nie odbija w ogóle i możemy zatrzymać iteracje dla tego piksela i przejść do następnego. Jeśli współczynnik ten jest większy niż 0 to cześć światła dla tej powierzchni będzie obliczona poprzez wysłanie nowego promienia z nowego punktu startowego na powierzchni.
float reflet = 2.0f * (viewRay.dir * n); viewRay.start = newStart; viewRay.dir = viewRay.dir - reflet * n;
Oczywiście ograniczmy liczbę odbić dla jednego piksela żeby uniknąć nieskończonej pętli.
Supersampling to metoda która ma na celu zmniejszyc efekt aliasingu (efekt schodkowania linii prostych) poprzez uzyskanie większej ilości próbek kolorów. Jeżli będziemy chcieli uzyskać efekt supersampling 4x, to dla obrazu o rodzielczość X,Y renderujemy wiekszy obraz X*2, Y*2 i bierzemy średnią z danej próbki żeby uzyskac kolor dla okerślonego piksela.
for (int y = 0; y < myScene.sizey; ++y) for (int x = 0; x < myScene.sizex; ++x) { float coef = 0.25f; float red = 0, green = 0, blue = 0; for(float fragmentx = x; fragmentx < x + 1.0f; fragmentx += 0.5f) for(float fragmenty = y; fragmenty < y + 1.0f; fragmenty += 0.5f) { // Każdy promień to 1/4 wartości całego piksela. float sampleRatio=0.25f; } // Udział każdego promienia jest dodawany i wynik jest zapisywany do pliku. red += sampleRatio * red; green += sampleRatio * green; blue += sampleRatio * blue; }
Koncepcja polega na rozłożeniu obliczeń na poszczególne rdzenie co powinno przyspieszyć proces renderowania.
Program uruchamiany na PPU wczytuje plik sceny zawierający listę obiektów, świateł i materiałów. Otrzymujemy strukturę zawierającą całą scenę.
scene myScene; if (!init(argv[1], myScene)) { cout << "Failure when reading the Scene file." << endl; return -1; }
Funkcja init wczytuje plik sceny podany jako parametr do programu. Zwraca strukturę zawierającą całą scenę.
Po inicjalizacji SPU wysyłany jest do niego kontekst zawierający dane wejściowe. Natomiast dla danych wyjściowych na PPU tworzona jest tablica, a jej adres wysyłany jest do poszczególnych SPU.
rc = posix_memalign ((void**)(&out_data), 128, myScene.sizex * myScene.sizey * 3 * sizeof(float)); if (rc != 0) { fprintf (stderr, "Failed allocating space for output data array\n"); exit (1); } memset (out_data, 0, myScene.sizex * myScene.sizey * 3 * sizeof (float));
NAstępnie obliczana jest część ekranu którą ma wyrenderować pojedynczy SPU.
part = myScene.sizey/spu_num
Jest to obliczane na podstawie wysokości sceny i zadanej ilości SPU (domyślnie 6).
Następnie w pętli wykonywanej tyle razy ile jest SPU, do kontekstu zapisywane są informacje o współrzędnych renderowanego fragmentu. Dodawany jest również adres tablicy wyjściowej, generowany na podstawie wielkości sceny, ilości SPU i obliczonej wcześniej części. Ponieważ struktura sceny zawiera wektory struktur opisujących obiekty , światła i materiały. Nie da się tego przesłać poprzez kontekst do SPU, trzeba było przekształcić wektor na tablice i w tej formie go przesłać. Dodatkowe zmienne w kontekście zawierają długości wektorów sceny. Następnie zapisywany jest aktualny czas, tworzony jest wątek na SPU oraz oczekiwanie na jego zakończenie i zatrzymywany czas.
for(i=0;i<spu_num;i++) { ctxs[i].Xstart = 0; ctxs[i].Xend = myScene.sizex; ctxs[i].Ystart = part*i; ctxs[i].Yend = part*(i+1); ctxs[i].spid = i; ctxs[i].out_addr = (unsigned long long)out_data + (unsigned long long)(i* part * myScene.sizex * 3 * sizeof(float)); for(int j=0;j<30;j++) { ctxs[i].mCont[j] = myScene.materialContainer[j]; ctxs[i].sCont[j] = myScene.sphereContainer[j]; ctxs[i].lCont[j] = myScene.lightContainer[j]; } ctxs[i].mSize = myScene.materialContainer.size(); ctxs[i].sSize = myScene.sphereContainer.size(); ctxs[i].lSize = myScene.lightContainer.size(); gettimeofday(&time,NULL); tstart=time.tv_sec +(time.tv_usec/1000000); pthread_create(&pthreads[i], NULL, &pthread_run_spe, &ctxs[i]); pthread_join (pthreads[i], NULL); gettimeofday(&time,NULL); tend=time.tv_sec +(time.tv_usec/1000000); }
Po zakończeniu pracy wszystkich SPU otrzymujemy tablice zawierającą opis pliku wynikowego. Uruchamiana jest funkcja Draw która zapisuje tablice jako plik graficzny w formacie TGA.
Na koniec programu wyświetlany jest czas samego renderowania
Struktura kontekstu:
struct context{ int spid; int Xstart,Xend,Ystart,Yend; int mSize,sSize,lSize; material mCont[30]; sphere sCont[30]; light lCont[30]; unsigned long long out_addr; }__attribute__((aligned(16)));
Na początku definiowana jest lokalna tablica w której przechowywana jest paczka zawierająca wyrenderowane piksele. SPU nie odsyła całego wyrenderowanego fragmentu tylko dzieli go na mniejsze paczki. SPU odbiera przesłany z PPU kontekst.
mfc_get(&ctx, parm, sizeof(ctx), tag,0,0); mfc_write_tag_mask(1<<tag); mfc_read_tag_status_all();
Jak wyżej zostało opisane scena została wysłana jako tablica i teraz należy tablice z powrotem włożyć do wektora struktur.
std::vector<material> materialki(ctx.mSize); std::vector<sphere> sfery(ctx.sSize); std::vector<light> swiatla(ctx.lSize); for(i=0;i<ctx.mSize;i++) { materialki[i].reflection = ctx.mCont[i].reflection; materialki[i].red = ctx.mCont[i].red; materialki[i].green = ctx.mCont[i].green; materialki[i].blue = ctx.mCont[i].blue; } for(i=0;i<ctx.sSize;i++) { sfery[i].pos.x = ctx.sCont[i].pos.x; sfery[i].pos.y = ctx.sCont[i].pos.y; sfery[i].pos.z = ctx.sCont[i].pos.z; sfery[i].size = ctx.sCont[i].size; sfery[i].materialId = ctx.sCont[i].materialId; } for(i=0;i<ctx.lSize;i++) { swiatla[i].pos.x = ctx.lCont[i].pos.x; swiatla[i].pos.y = ctx.lCont[i].pos.y; swiatla[i].pos.z = ctx.lCont[i].pos.z; swiatla[i].red = ctx.lCont[i].red; swiatla[i].green = ctx.lCont[i].green; swiatla[i].blue = ctx.lCont[i].blue; } myScene.materialContainer = materialki; myScene.sphereContainer = sfery; myScene.lightContainer = swiatla;
Gdy scena jest już poskładana i ma strukture taką samą jak przed podziałem następuje renderowanie. Po wygenerowaniu trzech subpikseli są one zapisywane w zdefiniowanej wcześniej tablicy lokalnej. Następnie sprawdzane jest czy lokalna tablica jest pełna, jeżeli tak tablica przesyłana jest w wyznaczone miejsce do tablicy głównej na PPU
local_buffer[pos] = blue; pos++; local_buffer[pos] = green; pos++; local_buffer[pos] = red; pos++; num+=3; if(pos >= 3072) { out_addr = ctx.out_addr + (page * 3072 * sizeof(float)); mfc_put (local_buffer, out_addr, 3072 * sizeof(float), tag, 0, 0); mfc_write_tag_mask (1 << tag); mfc_read_tag_status_all (); pos = 0; page++; }
W projekcie została także wykorzystana wektoryzacja, to z założenia powinno przyspieszyć obliczenia.
Kod który został zwektoryzowany:
red += sampleRatio * red; green += sampleRatio * green; blue += sampleRatio * blue;
Kod po zmianach:
vector float kolor; vector float kolory; vector float coefTemp = (vector float){0.25f, 0.25f, 0.25f, 0.25f}; . . kolory = (vector float){red, green, blue, 0.0f}; // przypisanie kolorow do wektora . . kolor = spu_madd(kolory,coefTemp,kolor); // mnozenie i dodawanie kolorow
Specjalnie stworzylismy bardziej skomplikowaną scene aby pokazać przyspieszenie obliczeniń.
Scena 1 | Scena 2 |
Tabelki poniżej pokazują czas jaki potrzeba do wygenerowania obrazu w zależności od ilości użytych rdzeni, oraz czy zostały użyta wektoryzacja.
Tabela 6.1. Wyniki pomiarów dla sceny 1
Ilośc rdzeni | Bez wektoryzacji [s] | Z wektoryzacją [s] |
---|---|---|
1 | 27.27 | 27.19 |
2 | 14.11 | 14.06 |
3 | 7.32 | 7.28 |
4 | 4.22 | 4.19 |
5 | 2.82 | 2.79 |
6 | 2.29 | 2.26 |
Wersja sekwencyjna | 13.41 |
Tabela 6.2. Wyniki pomiarów dla sceny 2
Ilośc rdzeni | Bez wektoryzacji [s] | Z wektoryzacją [s] |
---|---|---|
1 | 316.54 | 316.7 |
2 | 153.61 | 153.44 |
3 | 104.14 | 104.03 |
4 | 78.32 | 78.23 |
5 | 63.29 | 63.47 |
6 | 53.52 | 55 |
Wersja sekwencyjna | 142.91 |
Przeprowadziliśmy także testy na zwykłych komputerach domowych.
Tabela 6.3. Wyniki pomiarów na komuterach osobistych
Arek Linux | Grzegorz Linux | Arek Winwows | Grzegorz Windows | Konrad Windows | PS3 sekwencyjny | PS3 równoległy 1 SPU | PS3 równoległy 6 SPU | |
bez optmalizacji | 28.88 | 45.56 | 13.64 | 20.56 | 12.65 | 142.91 | 316.54 | 53.52 |
optymalizacja O3 | 7.69 | 8.88 | 21.84 | 11.05 | 1.91 |
Arek:
Intel Core 2 Duo E6300 1.83
ram 3Gb DDR2 667Mhz
Windows Vista Business x86
Użyty linux: andLinux działający pod systemem Windows Vista i używający wirtualizacji wbudowanej w procesor
Grzegorz:
AMD Athlon 64 3000+
ram 2 GB DDR2 667Mhz
Windows XP SP3 x86
Ubuntu 9.04 x64
Konrad:
AMD Athlon 64 X2 Dual Core 5200+ 2,61 Ghz x2
ram: 2 GB DDR 2 800 Mhz Dual Channel
Windows 7 Ultimate RC1
Patrząc na pomiary dla scen 1 i 2 można dojść do wniosku, że algorytm sekwencyjny jest szybszy od algorytmu równoległego wykonywanego na 1 SPU. Program sekwencyjny jest podan 2 razy szybszy. Przy 2 SPU programy renderują z porównywalną prędkością. Przyspieszenie zauważamy dopiero przy liczbie SPU 3 i większej. Jest to związane z różnicami pomiędzy procesorem PPU i SPU. Procesory SPU pracując razem dopiero osiągają większą moc od procesora głównego.
Algorytm renderingu nie przewidywał wektoryzacji co zmniejszyło możliwości modyfikacji. Większość operacji wykonywana jest na strukturach których nie dało się zamienić na wektory. Znalazł się tylko jeden kawałek kodu odpowiedzialny za mnożenie oraz dodawanie poszczególnych składowych pikseli w antialiasingu. Analizując czasu renderowania sceny 1 można zauważyć, że wektoryzacja zwiększa szybkość. Niestety spadek czasu renderowania nie przekracza pół sekundy. Gdyby w ten sposób renderować film składający się z wielu klatek oszczędność czasu była by znaczna. Natomiast analizując wyniki pomiarów dla sceny 2 (bardziej skomplikowanej) wyniki nie są już tak jednoznaczne. Czas renderowania wahał się. Przy niektórych pomiarach wektoryzacja zmniejszała, a w innych zwiększała czas renderowania. Każdy pomiar był przeprowadzany 3 razy, a dopiero średnia tych 3 pomiarów była zapisywana. W wypadku gdy wektoryzacja nie może być wdrożona w cały program nie można mówić o stałym zysku czasu renderowania.
Na koniec przeprowadzone zostało porównanie renderowania na konsoli PS3 oraz na komputerach członków zespołu. Program pod Windowsem został skompilowany pod Visual Studio 2005 jako wersja Release. Pod Linuksem natomiast przeprowadzono po 2 próby. Standardowa kompilacja g++ oraz kompilacja g++ z optymalizacją O3. Okazało się, że program pod linuksem z optymalizacją (nawet pod andLinux`em) jest szybszy od wersji Windowsowej. Ciekawa sytuacja wystąpiła przy analizie pomiarów przeprowadzonych na konsoli PS3. Przy standardowej optymalizacji program sekwencyjny okazuje się ponad dwukrotnie szybszy od wersji wykonywanej na 1 SPU. Natomiast przy optymalizacji O3 nastepuje odwrotna sytuacja. Wersja wykonywana na 1 SPU jest ponad dwukrotnie szybsza od wersji sekwencyjnej.
Zgodnie z tym co przedstawiliśmy można stwierdzić, że przy odpowiednim zaprojektowaniu i napisaniu aplikacji, równoległość przeyspiesza wykonywanie operacji. Pod uwagę trzeba wziąść stopień skomplikowania zadania, ilość dostepnych SPU oraz rozważyć alternatywne użycie mocnych komputerów klasy PC lub kart graficznych wykorzystujących technologię CUDA. Zbyt pochopna wektoryzacja bez przemyślenia tematu może spowodować, że zadanie będzie wykonywać się znacznie dłużej niż powinno.