Tämä koodi liittyy vahvasti tulevaan oppaaseen, joka valmistunee piakkoin.
Koodi on yksinkertainen "reaaliaikainen" toteutus raytracer -tekniikasta.
Se kykenee bilineaariseen tekstuurikartoitukseen, palloihin, tasoihin, phong kartoitukseen, pistemäisiin valoihin ja varjoihin.
Käyttää SDL kirjastoa.
Koodin kommentointi on vähäistä ja englanninkielistä, mutta kirjoittamani opas tulee selventämään kohtia.
Olen tekniikan opiskeluun käyttänyt flipcoden artikkelisarjaa ja koodi muistuttaakin joiltain osin sitä: http://www.flipcode.com/articles/article_raytrace01.shtml
Otan erittäin mielelläni tähänkin koodiin vastaan optimointiehdotuksia.
Käännetty ohjelma löytyy täältä: http://mbnet.fi/peku1/RaytraceSDL.rar
Raytrace.h
#define DOT(A,B) (A.x*B.x+A.y*B.y+A.z*B.z) #define NORMALIZE(A) {float l=InvSqrt(A.x*A.x+A.y*A.y+A.z*A.z);A.x*=l;A.y*=l;A.z*=l;} #define LENGTH(A) (sqrtf(A.x*A.x+A.y*A.y+A.z*A.z)) #define SQRLENGTH(A) (A.x*A.x+A.y*A.y+A.z*A.z) #define SQRDISTANCE(A,B) ((A.x-B.x)*(A.x-B.x)+(A.y-B.y)*(A.y-B.y)+(A.z-B.z)*(A.z-B.z)) #define EPSILON 0.0001f // Intersection method return values #define HIT 1 // Ray hit primitive #define MISS 0 // Ray missed primitive #define INPRIM -1 // Ray started inside primitive __inline float __fastcall InvSqrt (float x) { float xhalf = 0.5f*x; int i = *(int*)&x; i = 0x5f3759df - (i >> 1); x = *(float*)&i; x = x*(1.5f - xhalf*x*x); return x; } class Vector { public: float x,y,z; Vector() : x( 0.0f ), y( 0.0f ), z( 0.0f ) {}; Vector( float a_X, float a_Y, float a_Z ) : x( a_X ), y( a_Y ), z( a_Z ) {}; Vector Cross( Vector b ) { return Vector( y * b.z - z * b.y, z * b.x - x * b.z, x * b.y - y * b.x ); } void Normalize() { float len = InvSqrt(x*x + y*y + z*z); x *= len; y *= len; z *= len; } Vector operator+ (Vector& param) { Vector temp; temp.x = x + param.x; temp.y = y + param.y; temp.z = z + param.z; return temp; } Vector operator- (Vector& param) { Vector temp; temp.x = x - param.x; temp.y = y - param.y; temp.z = z - param.z; return temp; } Vector operator* (float param) { Vector temp; temp.x = x * param; temp.y = y * param; temp.z = z * param; return temp; } Vector operator* (Vector& param) { Vector temp; temp.x = x * param.x; temp.y = y * param.y; temp.z = z * param.z; return temp; } Vector operator/ (Vector& param) { Vector temp; temp.x = x / param.x; temp.y = y / param.y; temp.z = z / param.z; return temp; } Vector operator/ (float param) { Vector temp; temp.x = x / param; temp.y = y / param; temp.z = z / param; return temp; } }; typedef Vector Color; class Texture { public: Texture( Color* a_Bitmap, int a_Width, int a_Height ) : m_Bitmap( a_Bitmap ), m_Width( a_Width ), m_Height( a_Height ) {} Texture( char* a_File ) { FILE* f = fopen( a_File, "rb" ); if (f) { // extract width and height from file header unsigned char buffer[20]; fread( buffer, 1, 20, f ); m_Width = *(buffer + 12) + 256 * *(buffer + 13); m_Height = *(buffer + 14) + 256 * *(buffer + 15); fclose( f ); // read pixel data f = fopen( a_File, "rb" ); unsigned char* t = new unsigned char[m_Width * m_Height * 3 + 1024]; fread( t, 1, m_Width * m_Height * 3 + 1024, f ); fclose( f ); // convert RGB 8:8:8 pixel data to floating point RGB m_Bitmap = new Color[m_Width * m_Height]; float rec = 1.0f / 256; for ( int size = m_Width * m_Height, i = 0; i < size; i++ ) m_Bitmap[i] = Color( t[i * 3 + 20] * rec, t[i * 3 + 19] * rec, t[i * 3 + 18] * rec ); delete t; } } Color* GetBitmap() { return m_Bitmap; } Color GetTexel( float a_U, float a_V ) { // fetch a bilinearly filtered texel float fu = (a_U + 1000.5f) * m_Width; float fv = (a_V + 1000.0f) * m_Width; int u1 = ((int)fu) % m_Width; int v1 = ((int)fv) % m_Height; int u2 = (u1 + 1) % m_Width; int v2 = (v1 + 1) % m_Height; // calculate fractional parts of u and v float fracu = fu - int(fu); float fracv = fv - int(fv); // calculate weight factors float w1 = (1 - fracu) * (1 - fracv); float w2 = fracu * (1 - fracv); float w3 = (1 - fracu) * fracv; float w4 = fracu * fracv; // fetch four texels int v1w = v1 * m_Width; int v2w = v2 * m_Width; Color c1 = m_Bitmap[u1 + v1w]; Color c2 = m_Bitmap[u2 + v1w]; Color c3 = m_Bitmap[u1 + v2w]; Color c4 = m_Bitmap[u2 + v2w]; // scale and sum the four colors return c1 * w1 + c2 * w2 + c3 * w3 + c4 * w4; } int GetWidth() { return m_Width; } int GetHeight() { return m_Height; } private: Color* m_Bitmap; int m_Width, m_Height; }; class Ray { public: Ray() { m_Origin = Vector(0, 0, 0 ); m_Direction = Vector(0, 0, 0); } Ray( Vector& a_Origin, Vector& a_Dir ) { m_Origin = a_Origin; m_Direction = a_Dir; } void SetOrigin( Vector& a_Origin ) { m_Origin = a_Origin; } void SetDirection( Vector& a_Direction ) { m_Direction = a_Direction; } Vector& GetOrigin() { return m_Origin; } Vector& GetDirection() { return m_Direction; } private: Vector m_Origin; Vector m_Direction; }; class Material { public: Material::Material() { m_Color = Color( 0.2f, 0.2f, 0.2f ); m_Diff = 0.9f; m_Spec = 0.4f; m_Texture = 0; m_UScale = 1.0f; m_VScale = 1.0f; } void SetColor( Color& a_Color ) { m_Color = a_Color; } Color GetColor() { return m_Color; } void SetDiffuse( float a_Diff ) { m_Diff = a_Diff; } float GetSpecular() { return m_Spec; } void SetSpecular( float a_Spec ) { m_Spec = a_Spec; } float GetDiffuse() { return m_Diff; } void SetTexture( Texture* a_Texture ) { m_Texture = a_Texture; } Texture* GetTexture() { return m_Texture; } void SetUVScale( float a_UScale, float a_VScale ) { m_UScale = a_UScale; m_VScale = a_VScale; m_RUScale = 1.0f / a_UScale; m_RVScale = 1.0f / a_VScale; } float GetUScale() { return m_UScale; } float GetVScale() { return m_VScale; } float GetUScaleReci() { return m_RUScale; } float GetVScaleReci() { return m_RVScale; } private: Color m_Color; Texture* m_Texture; float m_Diff; float m_Spec; float m_UScale, m_VScale, m_RUScale, m_RVScale; }; class Primitive { public: Primitive() : m_Name( 0 ), m_Light( false ), m_cshadow( true ) {}; Material* GetMaterial() { return m_Material; } void SetMaterial( Material a_Mat ) { *m_Material = a_Mat; } virtual int Intersect( Ray& a_Ray , float *a_Dist ) = 0; virtual Vector GetNormal( Vector& a_Pos ) = 0; virtual Color GetColor( Vector& ) { return m_Material->GetColor(); } void SetName( char* a_Name ) { delete m_Name; m_Name = new char[strlen( a_Name ) + 1]; strcpy( m_Name, a_Name ); } char* GetName() { return m_Name; } virtual void Light( bool a_Light ) { m_Light = a_Light; } bool IsLight() { return m_Light; } virtual void ShadowCasting( bool a_cshadow ) { m_cshadow = a_cshadow; } bool IsShadowCasting() { return m_cshadow; } protected: Material* m_Material; char* m_Name; bool m_Light; bool m_cshadow; }; class CSphere : public Primitive { public: CSphere(Vector& a_Centre, float a_Radius ) { m_Centre = a_Centre; m_SqRadius = a_Radius * a_Radius; m_Radius = a_Radius; m_RRadius = 1.0f / a_Radius; // set vectors for texture mapping m_Vn = Vector( 0, 1, 0 ); m_Ve = Vector( 1, 0, 0 ); m_Vc = m_Vn.Cross( m_Ve ); m_Material = new Material(); } ~CSphere() { delete m_Material; } Vector& GetCentre() { return m_Centre; } float GetSqRadius() { return m_SqRadius; } int Intersect( Ray& a_Ray, float *a_Dist ) { Vector v = m_Centre - a_Ray.GetOrigin(); float b = DOT( v, a_Ray.GetDirection() ); if (b <= 0) return MISS; float det = (b * b) - DOT( v, v ) + m_SqRadius; int retval = MISS; if (det > 0) { det = sqrtf(det); float i1 = b - det; float i2 = b + det; //if (i2 > 0) //{ //if (i1 < 0) { if (i2 < *a_Dist) { *a_Dist = i2; retval = INPRIM; } } //else { if (i1 < *a_Dist) { *a_Dist = i1; retval = HIT; } } //} } return retval; } Color GetColor( Vector& a_Pos ) { Color retval; if (!m_Material->GetTexture()) retval = m_Material->GetColor(); else { Vector vp = (a_Pos - m_Centre) * m_RRadius; float phi = acosf(-DOT( vp, m_Vn ) ); float u, v = phi * m_Material->GetVScaleReci() * (0.31831f); //1.0f / 3.14159f float theta = (acosf(DOT( m_Ve, vp ) / sinf( phi ))) * (0.63662f); //2.0f / 3.14159f if (DOT( m_Vc, vp ) >= 0) u = (1.0f - theta) * m_Material->GetUScaleReci(); else u = theta * m_Material->GetUScaleReci(); retval = m_Material->GetTexture()->GetTexel( u, v ) * m_Material->GetColor(); } return retval; } Vector GetNormal( Vector& a_Pos ) { return (a_Pos - m_Centre) * m_RRadius; } private: Vector m_Centre; float m_SqRadius, m_Radius, m_RRadius; Vector m_Ve, m_Vn, m_Vc; }; class PlanePrim : public Primitive { public: PlanePrim( Vector& a_Normal, float a_D ) { D = a_D; N = a_Normal; m_UAxis = Vector( N.y, N.z, -N.x ); m_VAxis = m_UAxis.Cross( N ); m_Material = new Material(); } ~PlanePrim() { delete m_Material; } Vector& GetNormal() { return N; } float GetD() { return D; } int PlanePrim::Intersect( Ray& a_Ray, float* a_Dist ) { float d = DOT( N, a_Ray.GetDirection() ); if (d != 0) { float dist = -(DOT( N, a_Ray.GetOrigin() ) + D) / d; if (dist > 0) { if (dist < *a_Dist) { *a_Dist = dist; return HIT; } } } return MISS; } Vector GetNormal( Vector& a_Pos ) { return N; } Color GetColor( Vector& a_Pos ) { Color retval; if (m_Material->GetTexture()) { Texture* t = m_Material->GetTexture(); float u = DOT( a_Pos, m_UAxis ) * m_Material->GetUScale(); float v = DOT( a_Pos, m_VAxis ) * m_Material->GetVScale(); retval = t->GetTexel( u, v ) * m_Material->GetColor(); } else { retval = m_Material->GetColor(); } return retval; } private: Vector N; float D; Vector m_UAxis, m_VAxis; }; class Scene { public: Scene() : m_Primitives( 0 ), m_Primitive( 0 ) {}; ~Scene() { delete [] m_Primitive; delete [] m_Light; } void InitScene() { m_Primitive = new Primitive*[100]; m_Light = new Primitive*[100]; // ground plane m_Primitive[0] = new PlanePrim( Vector( 0, -1, 0 ), 30.0f ); m_Primitive[0]->SetName( "plane" ); m_Primitive[0]->GetMaterial()->SetDiffuse( 1.0f ); m_Primitive[0]->GetMaterial()->SetColor( Color( 0.4f, 0.3f, 0.3f ) ); m_Primitive[0]->GetMaterial()->SetTexture( new Texture( "wood.tga" ) ); m_Primitive[0]->GetMaterial()->SetUVScale( 0.05f, 0.05f ); m_Primitive[0]->ShadowCasting(false); // big sphere m_Primitive[1] = new CSphere( Vector( 40, -50, 120 ), 43 ); m_Primitive[1]->SetName( "big sphere" ); m_Primitive[1]->GetMaterial()->SetColor( Color( 0.7f, 0.7f, 0.7f ) ); m_Primitive[1]->GetMaterial()->SetTexture( new Texture( "marble.tga" ) ); m_Primitive[1]->GetMaterial()->SetUVScale( 0.8f, 0.8f ); // small sphere m_Primitive[2] = new CSphere( Vector( -80, 0, 90 ), 35 ); m_Primitive[2]->SetName( "small sphere" ); m_Primitive[2]->GetMaterial()->SetDiffuse( 0.6f ); m_Primitive[2]->GetMaterial()->SetColor( Color( 0.1f, 0.1f, 0.6f ) ); // back wall m_Primitive[3] = new PlanePrim( Vector( 0, 0, -1 ), 200.0f ); m_Primitive[3]->SetName( "plane2" ); m_Primitive[3]->GetMaterial()->SetDiffuse( 1.0f ); m_Primitive[3]->GetMaterial()->SetColor( Color( 0.4f, 0.3f, 0.3f ) ); m_Primitive[3]->GetMaterial()->SetTexture( new Texture( "marble.tga" ) ); m_Primitive[3]->GetMaterial()->SetUVScale( 0.01f, 0.01f ); m_Primitive[3]->ShadowCasting(false); // light source 1 m_Light[0] = new CSphere( Vector( -100, 0, 50 ), 1 ); m_Light[0]->Light( true ); m_Light[0]->GetMaterial()->SetColor( Color( 0.6f, 0.6f, 0.6f ) ); // light source 2 m_Light[1] = new CSphere( Vector( 100, 0, 0 ), 1 ); m_Light[1]->Light( true ); m_Light[1]->GetMaterial()->SetColor( Color( 0.7f, 0.7f, 0.9f ) ); // light source 3 /*m_Light[2] = new CSphere( Vector( 0, -70, -150 ), 1 ); m_Light[2]->Light( true ); m_Light[2]->GetMaterial()->SetColor( Color( 0.7f, 0.7f, 0.9f ) );*/ // set number of primitives m_Primitives = 4; m_Lights = 2; // 3 } int GetNrPrimitives() { return m_Primitives; } Primitive* GetPrimitive( int a_Idx ) { return m_Primitive[a_Idx]; } int GetNrLights() { return m_Lights; } Primitive* GetLight( int a_Idx ) { return m_Light[a_Idx]; } private: int m_Primitives; Primitive** m_Primitive; int m_Lights; Primitive** m_Light; };
Raytrace.cpp
#include <math.h> #include <time.h> #include <iostream> #include "SDL.h" #include "Raytrace.h" using namespace std; #define GRID_SIZE 2 #define SPACER 10 enum { SCREENWIDTH = 320, SCREENHEIGHT = 240, SCREENBPP = 32, SCREENFLAGS = SDL_HWSURFACE|SDL_DOUBLEBUF }; Vector Origin; float atime = 0.0f; Scene* sc; clock_t stime = 0; int frames = 0; int fps = 0; Vector precRays[SCREENWIDTH][SCREENHEIGHT]; float rotate3DZmat[361][4][4]; int grid[SCREENWIDTH/GRID_SIZE+1][SCREENHEIGHT/GRID_SIZE+1][3]; // This multiplies a vector with a matrice Vector mulMatrixVector(float a[4][4], Vector b) { Vector temp; temp.x = a[0][0]*b.x + a[0][1]*b.y + a[0][2]*b.z + a[0][3]; temp.y = a[1][0]*b.x + a[1][1]*b.y + a[1][2]*b.z + a[1][3]; temp.z = a[2][0]*b.x + a[2][1]*b.y + a[2][2]*b.z + a[2][3]; return temp; } __inline Vector __fastcall mulMatrixVectorRot3DZ(float a[4][4], Vector b) { Vector temp; temp.x = a[0][0]*b.x + a[0][1]*b.y; temp.y = a[1][0]*b.x + a[1][1]*b.y; temp.z = b.z; return temp; } Vector rotate3DZ(Vector start, float theta) { start = mulMatrixVectorRot3DZ(rotate3DZmat[int(theta * (360/3.14159))%360], start); return start; } Vector translate3D(Vector start, float dx, float dy, float dz) { start.x = start.x + dx; start.y = start.y + dy; start.z = start.z + dz; return start; } void DrawScene(SDL_Surface* surface) { if ( SDL_MUSTLOCK(surface) ) { if ( SDL_LockSurface(surface) < 0 ) { fprintf(stderr, "Can't lock the screen: %s\n", SDL_GetError()); return; } } atime += 0.1f; Primitive* pj = sc->GetLight(0); ((CSphere*)pj)->GetCentre().x = -100 + 100 * sin(atime); pj = sc->GetLight(1); ((CSphere*)pj)->GetCentre().z = 0 + 150 * cos(atime); ((CSphere*)pj)->GetCentre().y = 10 * sin(atime*5); pj = sc->GetPrimitive(2); ((CSphere*)pj)->GetCentre() = translate3D(((CSphere*)pj)->GetCentre(), 2*sin(atime), 0, 2*cos(atime)); pj = sc->GetPrimitive(1); ((CSphere*)pj)->GetCentre() = translate3D(((CSphere*)pj)->GetCentre(), 0, 2*sin(atime), 0); float atimeper4 = atime/4; for (int x=0;x<SCREENWIDTH/GRID_SIZE;x++) { for (int y=SPACER;y<SCREENHEIGHT/GRID_SIZE-SPACER;y++) { // tee säde kohti jokaista pikseliä Ray ra( Origin, rotate3DZ(precRays[x][y], atimeper4)); Color acc(0, 0, 0); float a_Dist = 10000000.0f; Primitive* prim = 0; int result = 0; // find the nearest intersection for ( int s = 0; s < sc->GetNrPrimitives(); s++ ) { Primitive* pr = sc->GetPrimitive( s ); int res; if (res = pr->Intersect(ra, &a_Dist)) { prim = pr; result = res; // 0 = miss, 1 = hit, -1 = hit from inside primitive } } if (result == 1) { // determine color at point of intersection Vector pi = ra.GetOrigin() + ra.GetDirection() * a_Dist; // trace lights for ( int l = 0; l < sc->GetNrLights(); l++ ) { Primitive* light = sc->GetLight(l); // handle point light source float shade = 1.0f; Vector L = ((CSphere*)light)->GetCentre() - pi; float tdist = LENGTH(L); L = L * (1.0f / tdist); Ray r = Ray(pi + L, L); for ( int s = 0; s < sc->GetNrPrimitives(); s++ ) { Primitive* pr = sc->GetPrimitive(s); if (pr->IsShadowCasting()) { if (pr->Intersect(r, &tdist)) { shade = 0; break; } } } if (shade != 0) { Color color = prim->GetColor( pi ); // calculate diffuse shading L = ((CSphere*)light)->GetCentre() - pi; L.Normalize(); Vector N = prim->GetNormal(pi); float dotnl = DOT(N, L); if (prim->GetMaterial()->GetDiffuse() > 0) { if (dotnl > 0) { float diff = dotnl * prim->GetMaterial()->GetDiffuse() * shade; // add diffuse component to ray color acc = acc + color * light->GetMaterial()->GetColor() * diff; } } // determine specular component if (prim->GetMaterial()->GetSpecular() > 0) { // point light source: sample once for specular highlight Vector V = ra.GetDirection(); Vector R = L - N * dotnl * 2.0f; float dot = DOT(V, R); if (dot > 0) { float spec = powf(dot, 20) * prim->GetMaterial()->GetSpecular() * shade; // add specular component to ray color acc = acc + light->GetMaterial()->GetColor() * spec; } } } } grid[x][y][0] = acc.x*255; grid[x][y][1] = acc.y*255; grid[x][y][2] = acc.z*255; } else { grid[x][y][0] = 0; grid[x][y][1] = 0; grid[x][y][2] = 0; } } } // pointer to the pixels Uint8 *p = (Uint8 *)surface->pixels; int r,g,b,ym,yp,xp,xm; int fff = SPACER/GRID_SIZE; int fff2 = (SCREENHEIGHT)/GRID_SIZE-1-SPACER; for (int x=1;x<SCREENWIDTH/GRID_SIZE-1;x++) { xp = x+1; xm = x-1; int x3 = GRID_SIZE*x; for (int y=fff;y<fff2;y++) { int y3 = GRID_SIZE*y; ym = y-1; yp = y+1; r = (grid[xp][y][0]+grid[x][yp][0]+grid[x][ym][0]+grid[xm][y][0])>>2; g = (grid[xp][y][1]+grid[x][yp][1]+grid[x][ym][1]+grid[xm][y][1])>>2; b = (grid[xp][y][2]+grid[x][yp][2]+grid[x][ym][2]+grid[xm][y][2])>>2; Uint32 clr = (r << 16) | (g << 8) | b; for (int u=0;u<GRID_SIZE;u++) { for (int v=0;v<GRID_SIZE;v++) { *(Uint32 *)(p + (y3+v) * surface->pitch + ((x3+u) << 2)) = clr; } } } } // remove lock if ( SDL_MUSTLOCK(surface) ) { SDL_UnlockSurface(surface); } if((clock() - stime) % CLK_TCK == 0) { if (clock() - stime != 0) { fps = frames / ((clock() - stime) / CLK_TCK); cout << "fps: " << fps << "\n"; } frames = 0; stime = clock(); } frames++; //flip the back buffer SDL_Flip(surface); } int main(int argc, char* argv[]) { // init SDL SDL_Init(SDL_INIT_VIDEO); atexit(SDL_Quit); // create window SDL_Surface* pSurface = SDL_SetVideoMode ( SCREENWIDTH, SCREENHEIGHT, SCREENBPP, SCREENFLAGS ); // set the origin Origin.x = 0; Origin.y = 0; Origin.z = -256; sc = new Scene(); sc->InitScene(); for (int i = 0; i < 361; i++) { rotate3DZmat[i][0][0] = cos(i*(3.14159/180)); rotate3DZmat[i][1][1] = cos(i*(3.14159/180)); rotate3DZmat[i][2][2] = 1; rotate3DZmat[i][3][3] = 1; rotate3DZmat[i][0][1] = -sin(i*(3.14159/180)); rotate3DZmat[i][1][0] = sin(i*(3.14159/180)); } for (int x=0;x<SCREENWIDTH/GRID_SIZE;x++) { for (int y=SPACER;y<SCREENHEIGHT/GRID_SIZE-SPACER;y++) { Vector dir = Vector(x-(SCREENWIDTH/2/GRID_SIZE), y-(SCREENHEIGHT/2/GRID_SIZE)-SPACER, 0) - Origin; dir.Normalize(); precRays[x][y] = dir; } } SDL_Event event; for (;;) { if ( SDL_PollEvent ( &event ) ) { if ( event.type == SDL_QUIT ) break; if ( event.type == SDL_KEYDOWN ) if ( event.key.keysym.sym == SDLK_ESCAPE ) break; } DrawScene(pSurface); } SDL_FreeSurface(pSurface); delete sc; SDL_Quit(); return 0; }
Näh, ei jaksa yrittääkää korjata että kääntyis linuxille (http://paste.afraid.org?9d8449488514796fe370de382e796f5e). Jäi sit näkemättä. No ei harmita :P
Ens kerralla voisit kertoa mitkä filut noi listaukset on ettei tarttis ihmetellä vähääkään aikaa. :)
ei millään pahalle mutta oli vähän epäselvän näköinen tuo ohjelma
Tarkoitatko nyt koodia vai itse ohjelmaa?
Ohjelma on epäselvä, sillä pikseliä kohti vaadittavien laskutoimituksien määrä on niin valtava, että on pakko hyppiä joidenkin pikselien yli ja sitten lopussa pehmeästi levittää pikselit toistensa päälle. Resoluutiokin on pieni, jotta saisin ohjelman pelaamaan reaaliajassa vähän heikommillakin koneilla. (omalla fps 40-65 tällä resoluutiolla)
hyvä esimerkki.
btw, kiitos hyvästä linkistä :)
Melkoisen viehkeä esimerkki :)
Upea.
Aihe on jo aika vanha, joten et voi enää vastata siihen.