piątek, 28 sierpnia 2009

Praca magisterska, md5 i nokia 3120 clasic

Dawno nic nie napisałem, ale prawda jest taka, że nie było o czym. Założenie było takie, że piszę tutaj o tym co uda mi się stworzyć w świecie programowania, a od ostatniego posta nie powstało nic. Aż do dziś ;)

Przez ostatnie niemal 2 tygodnie pisałem swoją pracę magisterską. Wciąż jestem w polu, ale w sumie wygląda to coraz lepiej. Już dawno postanowiłem, że napiszę ją w latex'u i jak na razie nie żałuje decyzji. Pisze się naprawdę sprawnie. Najfajniejsze jest to, że nie ma problemów z aktualizacją obrazków i bardzo łatwo jest rozbić cały dokument na osobne piliki. Boli tylko brak sprawdzania pisowni. Jakoś nie udało mi się tego skonfigurować. Teoretycznie Eclipse z dodatkiem Texclipse potrafi skorzystać ze słowników aspell. Niestety sprawa robiła się o kodowanie dokumentu. Nie jest to na szczęście zbyt bolesne, bo wciąż można skorzystać z innego softu, choćby open office'a.

Zanim zacząłem pisać pracę walczyłem jeszcze trochę z tym ładowaniem plików md5. Lipa. Coś mi tu umyka. Nie wiem, czy to błąd w kodzie, czy nie zrozumienie jakiegoś aspektu, np jak posługiwać się kwaternionami. W każdym bądź razie lipa. Udało mi się doprowadzić do tego, że szkielet mojego modelu animował się poprawnie. Jednak gotowiec ma coś nie tak z orientacją. Pozycje są poprawne. Brakło mi czasu, żeby problem rozwiązać.

Co do tego wszystkiego ma Nokia 3120 clasic ? Ano jest pretekstem do spłodzenia tego posta. Otóż mój szanowny rodziciel, wszedł w posiadanie tego modelu, drogą podpisania umowy z jednym z operatorów sieci gsm w Polsce. Telefon potęgą nie jest, czego się zresztą spodziewać po gratisie. Nie spodziewałem się jednak, że aż tak.
Wydawałoby się prosta sprawa skopiować kontakty z jednego telefonu na drugi. Poprzednik to sony erricson. Oba telefony posiadają moduł bluetooth. No to kopiujemy całą listę przez BT i po bólu ;)
Otóż nie. SE potrafi wysłać wszystkie kontakty na raz w formie pliku vcf. Nokia natomiast takiego pliku zrozumieć nie potrafi. Grrr.
Szybko okazało się, że problemem jest to iż jest to lista. Pojedynczą wizytówkę można przesłać. Czyli co, na SE strzałka w dół > opcje > wiecej > zaawansowane > wyslij > bt, Nokia > akceptuj > zapisz. I tak 380 razy. Masakra.
Po długim kombinowaniu wyszło, że takie wizytówki można wysyłać także z komputera. Byleby nie była to lista, a pojedyncze wpisy. Na szybko napisałem kawałek kodu do konwersji listy na osobne pliki.

Nie zagłębiałem się przy tej okazji nadmiernie w strukturę pliku .vcf. Także raczej nie należy się na to powoływać. Programik zadziałał zgodnie z oczekiwaniem, a cała operacja zakończyła się pełnym sukcesem, dając tym samym Nokii pierwszą szansę na dłuższe testy u mnie w domu. Wcześniej była tylko jedna i wytrzymała jeden dzień, zanim mnie szlag trafił ;)

/*
* main.cpp
*
* Created on: 2009-08-28
* Author: Adam Śmigielski
* E-mail: adam.smigielski@gmail.com
* Copyright: Adam Śmigielski
*/


#include <fstream>
#include <iostream>
#include <stdio.h>

using namespace
std;

int
main(int argc, char * argv[])
{

const
char * filename = 0;
if
(2 >= argc)
{

filename = argv[1];
cout << "Plik: " << filename << endl;
}

else

{

cout << "Podaj nazwe pliku" << endl;
return
1;
}


fstream in;
fstream out;
in.open(filename, fstream::in);
if
(false == in.is_open())
{

cout << "Plik nie istnieje" << endl;
return
2;
}

else

{

cout << "Otwarto plik" << endl;
}


char
buffer[256];
unsigned int
count = 0;
char
name[] = "result\\xxxx.vcf";
char
rev[] = "REV:20090828T071515Z";
int
revsize = strlen(rev);

while
(true)
{

in.getline(buffer, 256);

cout << buffer << endl;

if
(0 == strcmp(buffer, "BEGIN:VCARD"))
{

sprintf(name + 7, "%04d.vcf", count++);
cout << "Nazwa pliku: " << name << endl;

out.open(name, fstream::out);
if
(false == out.is_open())
{

cout << "Nie udalo sie otworzyc pliku" << endl;
return
2;
}

else

{

cout << "Otwarto plik" << endl;
}


out << buffer << endl;
while
(true)
{


in.getline(buffer, 256);

cout << buffer << endl;

if
(0 != strstr(buffer, "X-IRMC-LUID:"))
{

out << rev << endl;
}

else if
(0 == strcmp(buffer, "END:VCARD"))
{

out << buffer << endl;
break
;
}

else if
(0 != strstr(buffer, "TEL:"))
{

out << "TEL;CELL:" << buffer + 4 << endl;
}

else

{

out << buffer << endl;
}
}


out.close();
}

else if
(0 == strcmp(buffer, ""))
{

break
;
}
}


in.close();

return
0;
}



EDIT
Właśnie odkryłem, że mój sprytny sposób na zamieszczanie kodu wyświetla bool'e na czarno. Ehh, już zmienione. Teraz będą zielono niebieskawe :)

czwartek, 6 sierpnia 2009

Animacja szkieletowa

No temat masakra. Od poprzedniego postu cały czas walczę ze skiningiem. Na dobry początek zawiodły mnie szadery. Z jakiegoś, kompletnie nie zrozumiałego dla mnie, powodu pętla:
for (int i = 0; i < 4; i++)
wykonywała się tylko raz. Później, głównie dzięki przykładowi, zawartemu w Dx SDK, SkinMesh, udało mi się przekonać kompilator, że ta pętla to ma jednak 4 przebiegi. Co było przyczyną problemu wciąż nie wiem.
Następnie odkryłem, że kod (tym razem c++)
if (0 != flag & 1)
{
}

if
(0 != flag & 2)
{
}
...

if
(0 != flag & 32)
{
}
kompiluje się tylko do jednego, pierwszego, if'a. Cała reszta jest pomijana. Dzieje się tak przy -o0 -g3, a także dla domyślnych ustawień (czyt. bez flagi). Innych ustawień nie sprawdziłem. Nie mam pojęcia o co gcc chodziło. Czyżby bug ;)
Wyszło jeszcze kilka błędów. Wczoraj wieczorem udało mi się wyświetlić model w bind pose, ale ... . Ale wynik był zniekształcony. Geometria z całą pewnością jest poprawna, bo po ustawieniu macierzy jednostkowej wygląda jak należy. Czyli SUKCES, udało mi się w końcu zmusić wszystko do działania, a błędy muszą wynikać z nieprawidłowych macierzy.

Od początku idea aby wysyłać do karty "globalne" macierze dla każdej kości wydał mi się dziwny. Przecież to się musi rozjechać. Pomyślałem jednak, że może te wagi odpowiednio modyfikują transformację. Dzisiaj rano postanowiłem to przeanalizować matematycznie. No i oczywiście okazało się, że jest źle.

Weźmy taki przykład:
Punkt p [1, 1, 1], zaczepiony do dwóch kości, do obu po równo, czyli wagi 0,5
Kość A [0, 0, 0] zorientowana neutralnie (macierz jednostkowa)
Kość B [0, 0, 2] zorientowana neutralnie

p' = 0,5 * A * p + 0,5 * B * p
p' = [0,5 0,5 0,5] + [0,5 0,5 (0,5 * (1 + 2))] = [1, 1, 1,5]

A powinno wyjść [1, 1, 1]. Co z tym zrobić ? Bardzo UWAŻNIE przeczytać cokolwiek na ten temat. Na przykład ten temat na warsztacie. Odkryjemy w ten sposób, że macierze mają być "globalne", ale muszą uwzględniać pewien myk.

Na koniec screen, z bind pose:
Z Engine
Model pochodzi ze wspomnianej już strony: link

poniedziałek, 3 sierpnia 2009

MD5 mesh loader

Przebrnąłem przez napisanie funkcji ładującej model z pliku md5.

Na początek linki:
1 Eksporter do Blendera i modele
2 Opis formatu
3 Opis jak ładować z tego formatu
4 Temat na Warsztacie

A teraz moje uwagi i wnioski:
1. Dane joint'ów zapisane są względem początku układu, a nie rodzica. Aby wyznaczyć transformację względem rodzica należy skorzystać z tych równań:
p * q = g => q = p^-1 * g
p + v = g => v = g - p
p - parent, czyli dane rodzica
g - global, czyli dane zapisane w pliku
q i v - to kwaternion i wektor
p^-1 to oczywiście inwersja kwaternionu rodzica
2. Kwaterniony są zapisane w formie samych osi, bez w. Ja dowiedziałem się o tym z [3]. Skorzystano tu z faktu, że długość kwaternionu musi być równa 1. 1 = w*w + x*x + y*y + z*z. Chyba łatwo wyznaczyć ile wynosi w. Pamiętać tylko trzeba, że nie należy wyciągać pierwiastka kwadratowego z liczb ujemnych.
3. Pozycje wag są zapisane względem kości i to z uwzględnieniem orientacji. Aby wyznaczyć finalną pozycję, trzeba najpierw obrócić pozycję wagi o orientację kości. Dopiero tak obróconą translację możemy zsumować z pozycją kości. Wzór na rotację [3] punktu o kwaternion: a' = q * a * q^-1. Przy czym a i a' to kwaterniony z częścią rzeczywistą równą pozycji punktu i urojoną 0, trzeba o tym pamiętać.
4. Format nie przechowuje informacji o normalnych. Wyznacza się je oczywiście przy użyciu mnożenia wektorowego (cross product). Trzeba tu jednak wziąć pod uwagę, że indeksy wierzchołków są w CCW. To mnie zaskoczyło, ale tak mi wyszło. Nie wiem jednak czy to przypadkiem nie jest "wina" eksportera.
5. Plik ma 682 linijki. Czyli kupa roboty.

piątek, 31 lipca 2009

Optymalizcja gemetrii ciąg dalszy

Ze względu na pewne wątpliwości odnośnie licencji dla NvTriStrip, zdecydowałem się poszukać innego rozwiązania.

Założyłem nawet temat na Warsztacie.

Ostatecznie zdecydowałem się na Vertex cache optimization librar.

Efekty działania są jak najbardziej zadowalające.

czwartek, 30 lipca 2009

Optymalizacja geometrii

Ponieważ wczytany model zmasakrował moją aplikację, postanowiłem zainteresować się tym jak go zoptymalizować.

Jak na razie postanowiłem sprawić, aby stał się bardziej "cache friendly". Wyjaśnię co to znaczy, bo okazało się, że kolega programista nie zrozumiał.

Otóż procesor graficzny GPU, ma wbudowaną pamięć tzw. vertex cache. Przechowuje w niej wyniki obliczeń dla ostatnich N wierzchołków. Kluczem do ponownego użycia już otrzymanych wyników jest użycie indeksowanej geometrii.

Warunkiem jest tu jednak, aby już obliczone wierzchołki nie "wypadły" z cache'u. Odpowiednia organizacja bufora indeksów nie jest zadaniem trywialnym, postanowiłem więc skorzystać z gotowca: biblioteka NvtriStrip. Do biblioteki jest przykład użycia: NVTriStrip Test App.





Coprzedpo
Ilość narysowanych obiektów:2867328673
Średni czas jednego draw call'a [ms]:0.06882970.0721264
Średni czas jednej klatki [ms]:13.757111.497
Ilość draw call'i na klatkę:88
Średni fps:68.399581.8658
Co z tego wynika ?
1) Relacja pomiędzy fps, a czasem renderowania klatki jest wyraźna. Czyli dobrze, bo to znaczy, że system nie spędza za dużo czasu poza samym renderingiem.
2) Draw call, o ile wiem jest wykonywany asynchronicznie. CPU nie czeka na jego zakończenie. Pomiary nijak się mają do czasu rysowania całej klatki. To też dobrze wróży, bo oznacza, że CPU ma lepsze rzeczy do roboty niż zlecanie kolejnych zadań do narysowania. Mam tu też pewien, spory jak sądzę, zapas mocy.
3) Zyskałem ~2.3 milisekundy. Co znaczy, że optymalizacja geometrii dała zysk rzędu 16 %. Jednak ta wartość jest zakłamana, ponieważ pixel shader do lekkich nie należy. Uruchomiłem aplikację w perfHud, ale za dużo nie udało mi się wyciągnąć sensownych informacji. Wiem tylko, że % VS z 40 spadł do 30. Niestety nie za bardzo wiem jak to dokładnie interpretować.
4) 1 + 2 + 3 moja grafika ledwo zipie ;)

środa, 29 lipca 2009

Instancing ciąg dalszy

Tak jak sądziłem przesunięcie analizy kolejki obiektów do wyrenderowania o jeden stopień wyżej poprawiło nieco osiągnięty wynik:
Ilość narysowanych obiektów: 28673 - 28673
Średni czas jednego draw call'a: 0.0054 ms - 0.00342 ms
Średni czas jednej klatki: 19.02 ms - 12.46 ms
Ilość draw call'i na klatkę: 8 - 8
Średni fps: 49.3 - 69.9

To dobrze. Chciałoby się podnieść ten etap jeszcze o jeden poziom wyżej, ale to chyba nie możliwe. Założenie jest takie, że dla każdego punktu obserwacji (kamery, światła) do kolejki trafią inne obiekty, więc ich wspólna analiza nie za bardzo wchodzi w grę.

Mój początkowy zachwyt nad tymi wynikami spotkał się dziś z lodowatym prysznicem :) Skończyłem pisać ładowanie plików ASE i zamieniłem sześcian na sześcian z wyciętym środkiem. Źródło inspiracji: ten projekt. Spodobał mi się, bo ten w sumie prosty model ma tę cechę, że rzuca cień sam na siebie. Model i render wykonany w blenderze przeze mnie:
Z Engine

Jak się jednak okazało, model wcale taki prosty nie jest. 97 trójkątów i 105 wierzchołków !!! Pewnie dałoby się to nieco poprawić, ale na drastyczne zmiany bym nie liczył. Tak czy siak, z 70 fps zrobiło się przerywane ~25 (trochę płynnie, trochę szarpie)

Literatura:
Opis formatu ASE
Plugin do blendera

środa, 22 lipca 2009

Instancing

Dziś udało mi się zakończyć kod odpowiedzialny za stosowanie instancing'u.
Scena złożona z 4096 klocków, oświetlona światłem punktowym z wyliczaną dynamicznie mapą cienia (cube).

Klasyczne podejście:
Ilość narysowanych obiektów: 28673
Średni czas jednego draw call'a: 0.0026 ms
Średni czas jednej klatki: 84.91 ms
Ilość draw call'i na klatkę: 28673
Średni fps: 11.33

Instancing:
Ilość narysowanych obiektów: 28673
Średni czas jednego draw call'a: 0.0054 ms
Średni czas jednej klatki: 19.02 ms
Ilość draw call'i na klatkę: 8 !!!
Średni fps: 49.3

No i tu pojawia się pytanie. 8 razy 0.005 to przecież 0.04 milisekundy, czemu więc klatka trwa 20 milisekund ? To jest bagatela 500 razy dłuższy czas ! Mam taką teorię: bufory z macierzami do transformacji świata są nie optymalnie tworzone i wysyłane do karty.

Czemu tak sądzę ?
1. Aktualizuje shadow mapę
1a. Wyznaczenie macierzy PV
1b. Analiza danych pod kątem batch'y
1c. Przygotowanie bufora z macierzami (macierze są już wyliczone, trzeba tylko je skopiować do bufora)
1d. Ustawienie wszystkiego związanego z shaderami
1e. Draw call
1f. Jeśli został jeszcze jakiś kierunek w cube mapie idź do 1a (czyli 6 razy)
2. Aktualizacja kamery
2a. To samo co dla światła, tylko, że mamy jeden kierunek i 4 mrt.
2b. Rysowanie świateł

Łącznie 7 razy analizuję i kopiuje te same dane do karty graficznej. Konieczne są jednak tylko 2 razy.

Pożyjemy zobaczymy, muszę to sprawdzić, ale upał jest tak nieznośny, że wszystkiego się odechciewa.