SDL kotisivuilta löytyy tälläinen koodi yksittäisen pikselin piirtoon, mutta olisi hienoa, jos jossain selitettäisiin vielä paremmin sen toiminta periaateetta. Luulisi, että lähes kaikki pelin tekijät joutuu joskus sähläämään pikseli tasolla, niin miksiköhän yksittäisen pikselin piirrosta ei ole mitään lyhyttä opasta?
Ongelman korjaamiseksi selitän sen, minkä jo itse tiedän, mutta jätän kysymykseksi sen, jota en tiedä.
void putpixel(SDL_Surface *surface, int x, int y, Uint32 pixel) { int bpp = surface->format->BytesPerPixel; /* Here p is the address to the pixel we want to set */ Uint8 *p = (Uint8 *)surface->pixels + y * surface->pitch + x * bpp; switch(bpp) { case 1: *p = pixel; break; case 2: *(Uint16 *)p = pixel; break; case 3: if(SDL_BYTEORDER == SDL_BIG_ENDIAN) { p[0] = (pixel >> 16) & 0xff; p[1] = (pixel >> 8) & 0xff; p[2] = pixel & 0xff; } else { p[0] = pixel & 0xff; p[1] = (pixel >> 8) & 0xff; p[2] = (pixel >> 16) & 0xff; } break; case 4: *(Uint32 *)p = pixel; break; } }
Ja täällä selitykset/kommentointi:
void putpixel(SDL_Surface *surface, int x, int y, Uint32 pixel) { /* Fuktion parametrit: *surface: Funkitiolle täytyy antaa kahva siitä pinnasta, mihin pikseli piirretään. Eli tarvitsee osoittimen näytön pikseleihin. x: pikselin x-koordinaatti y: pikselin y-koordinaatti pixel: Pikselin väriarvo muodossa ?? Minkätakia pitää olla Uint32, eikö Uint8 riittäisi?? int bpp = surface->format->BytesPerPixel; /* kaivetaan näytön jäsenmuuttuja BytesPerPixel, joka kertoo montako tavua *surface(screeni) käyttää kuvamaan yhtä pikseliä. Minimissään pinta käyttää 1:den tavun ja maksimissaan 4 tavua. Tämä vaihtelee koneittain ja ominaisuus on olemassa sen takia, että esim. 4 tavun pikselillä saadaan enemmän värejä, kuin 1 tavun pikselillä. /* Here p is the address to the pixel we want to set */ Uint8 *p = (Uint8 *)surface->pixels + y * surface->pitch + x * bpp; /* surface->pixels osoittaa isoon yksiuloitteeseen taulukkoon, jossa on kaikki näytön pikselit. surface->pixels palauttaa siis vain osoitteen ensimmäisestä pikselistä näytöllä, eli vasemmassa yläkulmassa olevan pikselin osoitteen. surface->pitch palauttaa integerin, joka on yhtäsuuri kun pikseleiden lukumäärä horisontaalisesti(onko näin?). Kertomalla tämä lukumäärä y:llä, päästään oikealle riville y-akselin suhteen. lisäämällä x * bpp, päästään oikealle riville x-akselin suhteen, lukumäärä kerrotaan bpp:llä, koska taulukko, johon surface-pixels osoittaa, on muotoa Uint8, eli 8 bittinen(1 tavunen) integeri taulukko(tai siis yhden alkion koko on 8 bittiä). Joudumme kertomaan x:n bytesPerPixelillä, jotta osoitin pomppisi riittävän määrän eteenpäin. Jos bpp = 1, niin silloin lisäämällä osoittimeen 1, mennään yksi pikseli eteenpäin. Jos bpp = 2, niin lisäämällä osoittimeen yksi, mennäänkin vain puolikas pikselin eteenpäin - tämän takia x kerrotaan bpp:llä. */ switch(bpp) { case 1: *p = pixel; break; /* nyt *p osoittaan oikeaan kohtaan ja täytyy vain vaihtaa sen väriä. kysymys kuitenkin kuuluu, miten "pixel", joka on tyyppiä Uint32, ängetään piemempään integeriin, p:hen, joka muotoa *Uint8. Eikö tästä pitäisi seurata se, että loput 24 bittiä, jotka eivät mahdu enää 8 bittiseen integeriin, täyttäisivät seuraavat kolme alkiota taulukosta? */ case 2: *(Uint16 *)p = pixel; break; //Osoitin muutetaan 16 bittiseksi, jos Bytesperpixeli on 16 bittiä. Miksi? case 3: if(SDL_BYTEORDER == SDL_BIG_ENDIAN) { p[0] = (pixel >> 16) & 0xff; p[1] = (pixel >> 8) & 0xff; p[2] = pixel & 0xff; } else { p[0] = pixel & 0xff; p[1] = (pixel >> 8) & 0xff; p[2] = (pixel >> 16) & 0xff; } break; /* nämä toiminnot menee täysin ylihilseen. Tehdään jotain bittitason opetaarioita, jotta saataisiin väriarvot oikein. Sitten ne maskataan jollain maskilla 0xFF. Tämän täsmällinen ymmärtäminen olisi kuitenkin tarpeellista, jos haluaa itse tehdä omia pikselin piirto funktioita. */ case 4: *(Uint32 *)p = pixel; break; } //En ymmärrä yhtään enempää, kuin kohdassa 2. }
Uint8 riittää ainoastaan 8-bittisille väriarvoille. Uint32 sisältää 32 bittiä, siis 4 tavua, jotka ovat R, G, B ja alpha. Surfacen bpp-arvo riippuu surfacesta itsestään. Voit valita surfacellesi minkä vain bpp-arvon (tietyistä arvoista). surface->pitch palauttaa vaakasuuntaisen tavumäärän, muuten aivan oikein.
lainaus:
kysymys kuitenkin kuuluu, miten "pixel", joka on tyyppiä Uint32, ängetään piemempään integeriin, p:hen, joka muotoa *Uint8. Eikö tästä pitäisi seurata se, että loput 24 bittiä, jotka eivät mahdu enää 8 bittiseen integeriin, täyttäisivät seuraavat kolme alkiota taulukosta?
Tässä kopioidaan vain vain pixelin ensimmäinen tavu, koska tyyppi, johon p osoittaa (Uint8), on vain yhden tavun mittainen. Samasta syystä kohdassa 2 osoitin castataan Uint16*-tyyppiseksi, jotta pixelistä kopioitaisiin 2 tavua. Sama syy edelleen kohdassa 4.
Edit: Bittioperaatiot liittyvät yksittäisten tavujen erotteluun pixelistä. Bitshift-operaatiolla >> siirretään haluttu tavu luvun alkuun, jonka jälkeen andataan 0xff:llä (255) eli luvulla, jonka 8 ensimmäistä bittiä ovat ykkösiä ja loput nollia. Tällöin vain 8 ensimmäisen bitin arvot (eli halutun tavun) säilyvät ja muut nollataan.
Sijoitusoperaatiossa sijoitettava arvo muutetaan aina kohteen tyyppiin. Uint32:n sijoittaminen Uint8-muuttujaan tarkoittaa, että Uint32 muutetaan ensin Uint8-tyyppiseksi ja saatu arvo sijoitetaan. Aivan saman voit todeta esimerkiksi int- ja char-tyypeillä:
int a = 31619153; char teksti[4] = {0}; teksti[0] = a; // teksti[0] = (char) a; // Muunnos, jos char-muuttuja on 8-bittinen: // (char) a == (char) 31619153 == (char) (31619153 % 256) == (char) 81 == 'Q' // teksti[0] = 'Q'; // Muut kohdat eivät muutu!
Tuollaisen putpixel-funktion käyttö on äärimmäisen tehotonta: joka pikselin kohdalla on funktiokutsu ja hurja määrä tarkistuksia. Esimerkiksi peleissä on harvoin tarpeen käsitellä erilaisia pintoja, vaan voidaan etukäteen päättää, käytetäänkö 8-bittisiä pintoja (paletilliset kuvat) vai 32-bittisiä pintoja (täysvärikuvat läpinäkyvyyden kanssa) ja tehdä vain yksi lyhykäinen inline-funktio, jolla operaatio onnistuu. Näin pikselin sijoittaminen vie arviolta neljänneksen tai vähemmänkin siitä, mitä tuohon funktioon tuhlautuu.
Pitch on tavujen määrä rivillä. Tämä ei vastaa pikseleiden määrää, koska pikseli voi viedä monta tavua. Lisäksi rivin reunassa saattaa olla käyttämätöntä tilaa; jos pinta on laitteistokiihdytetty, voi olla tarpeen, että pitch on esimerkiksi neljällä jaollinen.
Ensiki, kiitokset vastauksista!
Sitten muutamia kysymyksiä molemmille:
lainaus:
Uint8 riittää ainoastaan 8-bittisille väriarvoille. Uint32 sisältää 32 bittiä, siis 4 tavua, jotka ovat R, G, B ja alpha. Surfacen bpp-arvo riippuu surfacesta itsestään. Voit valita surfacellesi minkä vain bpp-arvon (tietyistä arvoista). surface->pitch palauttaa vaakasuuntaisen tavumäärän, muuten aivan oikein.
Mutta lopputulos funktiossa on kuitenkin se, että 32 bittinen väriarvo castataan 8 bittiseksi(jos bpp = 1), jolloin väri muuttuu kokonaan, jos ensimmäinen tavu käsittelee pelkkää punaista?(Koska castissa otetaan vain eka tavu mukaan, eikö?)
toinen probleeema.
Sanotaan, että bytesperPixel on 2, niin silloin lauseke:
y * surface->pitch vie väärään paikkaan, koska jokaista y:tä kohdin liikutaan vain puolet vaadituista tavuista eteenpäin? koska pitch antaa vain tavumäärän ja se ei anna tupla määrää tavuja, jos bpp = 2? eikö lauseke pitäisi olla muotoa y*surface->pitch * bpp?
lainaus:
Bittioperaatiot liittyvät yksittäisten tavujen erotteluun pixelistä. Bitshift-operaatiolla >> siirretään haluttu tavu luvun alkuun, jonka jälkeen andataan 0xff:llä (255) eli luvulla, jonka 8 ensimmäistä bittiä ovat ykkösiä ja loput nollia.
Mutta mikä tämän BIG_ENDIAN systeemin pointti on? Miksi pitää tehdä tälläinen järjestelmä, että bittejä joudutaan siirteleen ennen kuin pikseliin päästää käsiksi?
lainaus:
Tuollaisen putpixel-funktion käyttö on äärimmäisen tehotonta: joka pikselin kohdalla on funktiokutsu ja hurja määrä tarkistuksia. Esimerkiksi peleissä on harvoin tarpeen käsitellä erilaisia pintoja, vaan voidaan etukäteen päättää, käytetäänkö 8-bittisiä pintoja (paletilliset kuvat) vai 32-bittisiä pintoja (täysvärikuvat läpinäkyvyyden kanssa) ja tehdä vain yksi lyhykäinen inline-funktio, jolla operaatio onnistuu.
Mutta jotta haluaisin tehdä omat putPixeli funktiot, niin mun on ymmärrettävä putPixelin toiminta periaate. Joudun käsittelemään pikselitasolla jokaista kuvaa, niin eikö ole mielekästä, että:
1. Kuvat hajotetaan väriarvo taulukoiksi.
2. Tehdään taulukoille kaikki tarvittavat prosessit, kuten rotatoinnut, valoefektit, liitokset toisiin kuviin jne.
3. Kuvat lätkitään(tai joitakin osia kuvista) suoraan näytölle esim sijoittemalla näitä väriarvotaulukoissa olevia pikseleitä SDL_surfaceen. Tekee jonkun loopin, jossa *näytön osoitinta kasvatetaan yhdellä ja sen arvoa muutetaan, niin eihän sitä nopeammaksi enää piirtäminen voi mennä. Esim.
//*Uint8 näyttö; osoittaa näytön ensimmäiseen pikseliin. //*Uint8 väriarvot; for(int i = 0; i < 800*600; i++) { *(näyttö + i) = *(väriarvot + i); }
Eihän taulukkoa, joka käsittää näytöllä olevat pikselit, voida enää muuttaa nopeammin, mitä kyseisessä loopissa muutetaan, vai? Vaikka kuinka tekisi grafiikka, niin eikö viimekädessä löydy aina operaatio, jossa näytön pikselille sijoitetaan uusi arvo, vai? Esim. vaikka flippaat kuvan näytölle sdl:ssä, niin jostain sieltä sisältää löytyy operaatio, jossa pikseleille sijoitetaan uusia arvoja? Jos ei SDL kirjastosta löydy tätä operaatiota, niin kuitenkin jollain tasolla viimekädessä löytyy sijoitus operaatio, jossa pikselille annetaan uusi arvo?
Paulus M kirjoitti:
Mutta lopputulos funktiossa on kuitenkin se, että 32 bittinen väriarvo castataan 8 bittiseksi(jos bpp = 1), jolloin väri muuttuu kokonaan, jos ensimmäinen tavu käsittelee pelkkää punaista?(Koska castissa otetaan vain eka tavu mukaan, eikö?)
Jos bpp on 1, väriarvo ei ole muotoa RGB(A) vaan indeksi, joka viittaa väriin ennalta määrätyssä paletissa
[/lainaus]
.
Paulus M kirjoitti:
Sanotaan, että bytesperPixel on 2, niin silloin lauseke:
y * surface->pitch vie väärään paikkaan, koska jokaista y:tä kohdin liikutaan vain puolet vaadituista tavuista eteenpäin? koska pitch antaa vain tavumäärän ja se ei anna tupla määrää tavuja, jos bpp = 2? eikö lauseke pitäisi olla muotoa y*surface->pitch * bpp?
Ei, vaan pitch antaa nimenomaan oikean tavumäärän, jossa tuo bpp on jo otettu huomioon.
Paulus M kirjoitti:
Mutta mikä tämän BIG_ENDIAN systeemin pointti on? Miksi pitää tehdä tälläinen järjestelmä, että bittejä joudutaan siirteleen ennen kuin pikseliin päästää käsiksi?
Big endian on yksi mahdollinen järjestys tavuille luvussa. Toinen, päinvastainen järjestys, on nimeltään little endian. Näillä sinänsä ei ole mitään tekemistä bittioperaatioiden kanssa. Jälkimmäistä kysymystä en ihan ymmärrä, totta kai tietokone joutuu pyörittelemään bittejä käsitelläkseen arvoja muistissa?
Täsmennetään nyt vielä, että esim. x86-prosessori käsittelee lukuja LE-muodossa ja PowerPC taas BE-muodossa.
Siitäkään ei ole takeita, että edes LE-koneissa alin tavu olisi R; esimerkiksi Windowsin BMP-tiedostoissa järjestys onkin BGR, ja SDL säilyttää tämän. Itse olen tavannut muuttaa kaikki pinnat 32-bittisiksi (luomalla uuden 32-bittisen pinnan ja kopioimalla kuvan SDL_BlitSurface-funktiolla tälle uudelle pinnalle).
Tässä on tiivistettyjä funktioita eri bittisyvyyksille:
static void putpixel_32(SDL_Surface *s, int x, int y, Uint32 c) { assert(0 <= x && x < s->w && 0 <= y && y < s->h); Uint8 *data = (Uint8*) s->pixels; Uint32 *row = (Uint32*) (data + y * s->pitch); row[x] = c; } static void putpixel_16(SDL_Surface *s, int x, int y, Uint16 c) { assert(0 <= x && x < s->w && 0 <= y && y < s->h); Uint8 *data = (Uint8*) s->pixels; Uint16 *row = (Uint16*) (data + y * s->pitch); row[x] = c; } static void putpixel_8(SDL_Surface *s, int x, int y, Uint8 c) { assert(0 <= x && x < s->w && 0 <= y && y < s->h); Uint8 *data = (Uint8*) s->pixels; Uint8 *row = data + y * s->pitch; row[x] = c; }
Kuten huomaat, mikään muu näissä ei vaihdu kuin rivillä olevan datan tyyppi (Uint32, Uint16, Uint8). Viimeisestä on lisäksi jätetty pois ylimääräinen tyypinmuunnos, koska Uint8* on jo valmiiksi Uint8*. Funktiot voisi siis luoda näppärästi yhdellä makrolla:
#define putpixel_n(n) \ static void putpixel_ ## n (SDL_Surface *s, int x, int y, Uint ## n c) { \ assert(0 <= x && x < s->w && 0 <= y && y < s->h); \ Uint8 *data = (Uint8*) s->pixels; \ Uint ## n *row = (Uint ## n *) (data + y * s->pitch); \ row[x] = c; \ } putpixel_n(8) putpixel_n(16) putpixel_n(32) #undef putpixel_n
Tai C++:n malleilla:
template <typename UintN> static void putpixel(SDL_Surface *s, int x, int y, UintN c) { assert(0 <= x && x < s->w && 0 <= y && y < s->h); Uint8 *data = (Uint8*) s->pixels; UintN *row = (UintN *) (data + y * s->pitch); row[x] = c; } // putpixel<Uint32>(s, 2, 4, 0x66cc99); #define putpixel_32 putpixel<Uint32> #define putpixel_16 putpixel<Uint16> #define putpixel_8 putpixel<Uint8>
24-bittinen pinta taas vaatii ylimääräistä kikkailua (bittisiirtoja), koska ei ole olemassa luonnostaan 24-bittistä muuttujaa. Siksi on käytännöllisempää ja tehokkaampaa käyttää 32-bittisiä pintoja.
Aivan, kiitoset vastauksista.
Tuosta Metabolixen koodista en kyllä ymmärrä tuota assert riviä, mutta muuten alkaa olee aika selkeä.
Sellainen vielä kysymys, että olisiko toi systeemi, jonka kirjoittelin, nopea tapa hallita grafiikkaa, vai pitäisikö mun yrittää pyöritellä jotain image luokkia? Miten ootte vääntänyt grafiikka, jos täytyy tehdä paljon pikselitason prosessointia?
Paulus M kirjoitti:
Tuosta Metabolixen koodista en kyllä ymmärrä tuota assert riviä, mutta muuten alkaa olee aika selkeä.
Standardikirjaston makro assert (C: <assert.h>, C++: <cassert>) tarkistaa, että annettu ehto on tosi, ja jos näin ei ole, tulostaa hieman tietoja ja lopettaa ohjelman.
testi.bin: testi.c:11: putpixel_32: Assertion `0 <= x && x < s->w && 0 <= y && y < s->h' failed. Aborted (SIGABRT)
Jos kuitenkin makro NDEBUG on asetettu (#define NDEBUG
tai vastaavasti kääntäjän komentoriviltä esim. -DNDEBUG
), kaikki assert-rivit häviävät koodista täysin. Se on siis käytännöllinen debug-väline, jonka saa helposti pois päältä siinä vaiheessa, kun koodi toimii. Tarkistaminen on sen verran hidasta, että noin kriittisessä kohdassa kuin yhden pikselin asettamisessa sitä ei kannata pitää päällä enää valmiissa ohjelmassa.
Tämä nimenomainen assert tarkistaa, että annetut x ja y ovat sallituissa rajoissa.
Kuvalle on käytännöllistä tehdä luokka, joka sisältää tarvittavan SDL_Surfacen luonnin ja tuhoamisen ja piirtää pikseleitä suoraan pinnalle. Suosittelen Uint32-tyyppisiä pikseleitä ja niiden ympärille vielä yksinkertaista rgba-rakennetta:
#include <cstdio> struct rgba { #if defined(SDL_BYTEORDER) && SDL_BYTEORDER == SDL_BIG_ENDIAN Uint8 a, b, g, r; #else Uint8 r, g, b, a; #endif rgba(): r(0), g(0), b(0), a(0xff) {} rgba(Uint8 _r, Uint8 _g, Uint8 _b, Uint8 _a = 0xff): r(_r), g(_g), b(_b), a(_a) {} rgba(Uint32 c) { *this = c; } operator Uint32 () { return *(const Uint32*)this; } rgba& operator = (Uint32 c) { *(Uint32*)this = c; return *this; } static const rgba& at(const Uint32& c) { return *(const rgba*)&c; } static rgba& at(Uint32& c) { return *(rgba*)&c; } }; int main() { Uint32 c = 0; rgba &x = rgba::at(c); x.r = 0xff; std::printf("%08x\n", c); rgba y; y.g = 0x99; std::printf("%08x\n", (Uint32) y); y = 0x334455; std::printf("%02x, %02x, %02x, %02x\n", y.r, y.g, y.b, y.a); }
Ok, kiitokset MetaBolix ajastasi.
Aihe on jo aika vanha, joten et voi enää vastata siihen.