Et starfield er en meget brugt effekt, yderst simpel, og nem at lave. Der er naturligvis
mange varriationer, mindst ligeså mange måder at gøre det på, og sværhedsgraden
svinger meget, alt efter hvilken slags, og hvormange ting man vil ligge i det.
Vi starter med et simpel horisontalt starfield. hvad er det egentlig vi skal lave.. nogle
dots (stars) som scroller fra højre mod venstre, eller omvendt, hvilket (i denne udgave)
betyder at det kun er positionen på x-aksen, der ændre sig, og y-værdien er den samme.
Det eneste vi skal huske er, hvor
dot'en befinder sig "i rummet". Da det er et 2D starfield, vil skræmpositionen
være lig positionen i rummet. For at gøre koden simpel, laver vi en struktur til en dot.
typedef struct
{
int xpos, ypos;
int speed;
}STAR;
Ikke overraskende er xpos og ypos positionen af dot'en, og speed er den fart dot'en
skal bevæge sig med.Inden vi starter skal vi have deklereret og initialiseret vores dots.
først laver vi et array af dots, og fylder positioner som ligger inden for skærmen ind,
på hver af dot'enes positions variabler... vi tager lige et eksempel:
#define RESX 320 // det er
bare så koden giver lidt mening at størrelsen på skærmen er
#define RESY 200 //
defineret. RESX og RESY kan naturligvis være hvilken somhelst opløsning.
#define MAXSPD 4
#define Nstars 1000
STAR star[Nstars];
for(int n=0;n<Nstars;n++)
{
star[n].xpos = rand()%RESX;
star[n].ypos = rand()%RESY;
star[n].speed= (rand()%MAXSPD)+1; // vi vil ikke have hastigheder der
er lig 0.
}
nu mangler vi bare at lave et loop, hvor vi i hver frame adderer hastigheden til
xpos. Vi skal også undersøge om dot'en er uden for skærmen. F.eks hvis xpos >= RESX
trækker vi RESX fra så dot'en kan starte forfra med at køre hen over skærmen.
Jeg bruger kun putpixel linierne for at demonstrerer, at vi plotter en dot, om det så er
til skærmen, eller til en virtuel buffer, er op til en selv.
for(int n=0;n
{
star[n].xpos+=star[n].speed;
if(star[n]>=RESX) star[n].xpos-=REX;
putpixel(star[n].xpos, star[n].ypos, color);
}
Hvis man ville gøre ens rutine lidt mere flexibel, kunne man gøre følgende:
star[n].xpos+=star[n].speed;
star[n].xpos%=REX;
så vil ens rutine virke uanset om man trækker hastigheden fra, eller ligger den
til, men tilgengæld bliver rutinen også langsommere, fordi en % (modulos) laver en
division, og dette bliver gjort pr. pixel - hver frame (meen det er jo et frit land :).
Ovenstående virker også kun, hvis man sletter hele skærmen hver frame (eller
vindue/område man opdaterer) Hvis man ikke ønsker dette, skal man slette den gamle dot
selv.
oldx = star[n].xpos;
..
putpixel(oldx,star[n].ypos, BACKGROUNDCOLOR);
putpixel(star[n].xpos,star[n].ypos, color);
grunden til vi ikke gemmer ypos, er fordi det kun er xpos der ændre sig, og derfor
ikke er nødvendigt.
Når ens lille starfield kører, vil vi hurtigt blive en anelse skuffet....
Alle dots'ne har samme farve, man begynder at kunne se mønstre i scrollen og man er
begrænset til MAXSPD "lag". Det var det mest
grundliggende, meeen... nej.. det er grimt.
En kæmpe forbedring man kan lave unden det store, er at lave lys-styrken til en funktion
af hastigheden. Jo hurtigere de er, desto mere lyser de, hvilket giver en "fake"
dybde fornemmelse.
Vi vil også gerne kunne styre hastighederne lidt bedre, f.eks. med komma tal, så en dot
kan bevæge sig med en halv pixel pr. frame. og for at toppe det af, kan vi med vores
kvarte, halve, hele etc. positioner lave pixlen antialiased, så bevægelsen bliver mere
flydende.
Det lyder som er stort spring, og en voldsom udbyggelse, og det er det også - men vi
tager det et skridt ad gangen.
vi starter med at lave vores struktur om.
typedef struct
{
float xpos, ypos, speed, brightness;
}STAR;
den positive ting. Det er uhyggelig nemt at flytte en dot 0.1 pixel pr. frame, da
speed bare kan være lig 0.1 (im so clever (citat: skurken fra Cats 'n' Dogs) :)
den negative ting. Man skal passe på hvad man tester på, og når vi skal plotte vores
dots. float til int konvertering
er IKKE nogen ultra fed ting. Det er langsomt, hvilket man ikke ønsker i en realtime
effekt - som normalt skal køre så hurtigt som overhovedet muligt.
Hvis vores RESX og RESY er
defineret som Int's, vil man blive tvunget til at lave en float2int konvertering når man
tester om man er nået uden for skærmen.
så vi laver vores defines om (hvis man har RESX og RESY i form af variabler fra en skræm-info-struct, eller fra et
grafik system, bør man lave 2 nye variabler som er floats - så man kun laver denne
konvertering een gang):
#define FRESX 320.0
#define FRESY 200.0
og ens speed initialisering:
star[n].speed = ((float)rand()/(float)RAND_MAX) *
MAXSPD;
Vi får nu en værdi mellem 0 og 1, vi kan gange op med voers max-speed. Et
alternativ til floats ville naturligvis være fixed-point (læs toturialen).
Nu da vores starfield er n-lag skal vi lige have en lille ting med. Hvergang en dot ryger
uden for skærmen, bør man beregne en ny hastighed, ny xpos og en ny ypos. på denne
måde vil der ikke komme mønstre, og ens starfield virker mere "uendelig". Man
kunne jo lave en funktion:
#define FIRSTTIME 0
#define NEWVAL 1
void newstar(STAR *s, char type)
{
s->ypos = ((float)rand()/(float)RAND_MAX) * FRESY;
s->spd((float)rand()/(float)RAND_MAX) * MAXSPD;
if(type==FIRSTTIME) s->xpos = ((float)rand()/(float)RAND_MAX) *
FRESX;
else s->xpos = 0.0;
}
så vil vores init rutine se noget lign. sådan her ud:
for(int n=0;n<Nstars;n++)
newstar(&star[n],FIRSTTIME);
i vores loop får vi så:
for(int n=0;n= FRESX) newstar(&Star[n], NEWVALUE);
putpixel(oldx, oldy, 0);
putpixel(star[n].xpos, star[n].ypos, color);
}
Nok ikke overraskende, men det er naturligvis nemt at lave om til at køre via
y-aksen. Her er det bare
star[n].ypos+=star[n].speed;
if(star[n].ypos >= FRESY) newstar(&Star[n], NEWVALUE);
newstar funktionen skal så bare tage hensyn til dette, og kunne da hurtigt laves
generel med et ekstra parameter.
så var det lys-itensiteten. Vi skal have scaleret hastigheden til at være mellem 0 og
255 - så kan vi bruge værdien direkte som farve i truecolor, eller som pallette-index i
gray-scale (256 col). (læs scaling) For at gøre det nemmere for os selv, beregner vi
lys-styrken hvergang vi laver en dot om, så i vores funktion newstar, under linien vi
får vores hastighed, kunne vi lave følgende:
s->brightness = (s->speed * 256.0) / MAXSPD;
man skal naturligvis huske at sætte sin pallette op (go figure).
Optimering venter vi med til en anden gang.
Nu er vores starfield i n-"lag", og farverne er justeret efter hastighederne
(fake depth). Der findes nogle tricks til at lave hurtig floating point til integer
konversion (læs tips n' tricks).
Vi vil gerne have mere flydende bevægelser istedet for at ens dots hopper en hel pixel ad
gangen.
Her skal vi bruge Antialiasing, eller Wu-pixels. Hvis man skal sætte en pixel MIDT på
skærmen, skal vi sætte den på (RESX/2)+0.5 og (RESY/2)+0.5. Det går ikke op, fordi midten på skærmen ligger
mellem 4 pixels.
Det vil sige at midten at skærmen er (320.5 , 240.5) - og det er ikke muligt at putte en
pixel/dot der.
Man kan så bruge en teknik der "simulerer" at vi sætter en dot mellem pixles
på skærmen. Her "vægter" man hvor meget dot'en skal lyse op over 4 pixels
istedet, hvor floor(star[n].xpos) og floor(start[n].ypos) bestemmer det øverste venstre
hjørne. Komma delen fortæller hvor meget af de egentlige pixels der er optaget af vores
dot. Vores Antialiased-/Wu-dot kan ikke dække mere end 4 pixels (et kvardrat). Disse 4
pixels vil tilsammen indeholde intensiteten af den dot vi vil sætte. Hvis xpos og ypos
ingen komma del har, vil den øverste venstre pixel være 100% af dot-intensiteten,
hvorimod hvis xpos og ypos havde en komma del på .5, ville de 4 pixels på skærmen hver
have 25% af dot-intensiteten.
Hvordan beregner vi det så. Nu ER vores positioner allerede i floats, så vi er allerede
godt på vej. Igen ville fixed point meget vel kunne optimerer hastigheden, men for
læsevenlighed og for forståelsens skyld, laver vi det igen i ren floats, og konverterer
til integer helt til sidst.
float x = floor(star[n].xpos);
float y = floor(star[n]-ypos);
float fx= star[n].xpos-x;
float fy= star[n].ypos-y;
float bn= star[n].brightness;
int index = (int) (x+y*FRESX);
Screen[index] = (1.0-fx) * (1.0-fy) * bn;
Screen[index+1] = fx * (1.0-fy) * bn;
Screen[index+RESX] = (1.0-fx) * fy * bn;
Screen[index+RESX+1]= fx * fy * bn;
Det eneste problem er at man skal passe på med pixels nederst på skærmen, for
hvis man tegner en pixel på (x, RESY-1) - vil ens Wu-pixel
tegne uden for skærmen og der med i noget ram man ikke skal pille ved.
Lige ved første øjekast ser "alt" forkert ud. Fordi vi ingen
"bevægelse" har på y-aksen og vi nu har kommatal i voresd y.koordinater, vil
en dot på f.eks 100.5 se ud som om den er 2 pixels høj. En anden ting man vil se er at
en dot kan se ud som om den blinker. Det er fordi vores hastighed stepper med et par
uheldige værdier, som gør at vores kommadel måske rammer 0.2, 1.4, 2.6 etc. så vil
vores dot's intensitet blive delt ud over 2
pixels, hvilket kan resulterer i at man får 2 mørke pixels frem for een lys.
Jeg kan næsten føle at folk tænker "Hvorfor så overhovede bruge antialias til et
2D starfield ?", og må da også indrømme at det nok ikke lige er den effekt
antialias er bedst egnet til - men det er den til det vi skal se på nu. det er et
starfield som kører ad z-aksen istedet for ad x-aksen, altså ind, eller ud af skærmen.
En måde at lave et Z-starfield på er at bruge polære koordinater. Men en vinkel og en
radius kan man bestemme hvor ens dots skal tegnes. Ved at lave en perspektiv beregning på
radius værdien, har man det man skal bruge.
typedef struct
{
float angel, radius, speed, brightness;
}ZSTAR;
Den ser jo bekendt ud, det er bare xpos og ypos som er byttet ud.
den store forskel er at vi beregner vores positioner, ved hjælp af angel og radius, da
disse ikke er vores faktiske positioner.
En ting vi skal huske er, at C regner i radianer, og hvis man vil det er det fint, men vi
omregner til grader.
#define RAD (PI/180.0)
I stedet for at ligge en værdi til xpos eller ypos er det nu radius vi vil
forøge. Under initialiseringen får hver dot en vinkel. Denne beholder den til dot'en er
uden for vores ønskede område. Vi beregner:
float x = cos(star[n].angel*RAD)*star[n].radius;
float y = sin(star[n].angel*RAD)*star[n].radius;
Men.. i et koordinat-system er centrum 0,0 - og på
skærmen er der øverste venstre hjørne :(
Så vi skal have centreret vores dot.
#define HALFRESX ((FRESX/2)+0.5)
#define HALFRESY ((FRESY/2)+0.5)
så bliver vores beregning:
float x =
cos(star[n].angel*RAD)*star[n].radius+HALFRESX;
float y = cos(star[n].angel*RAD)*star[n].radius+HALFRESY;
istedet for at checke på radius om vi er uden for skærmen, bryger vi den
beregnede værdi til at se om vores dot er på skærmen. Hvis ikke, får den nye værdier.
if( (x<0.0) || (y<0.0) || (x>=(FRESX-1.0)) ||
(y>=(FRESY-1.0)) )
{
newstar(&star[n],NEWVALUE);
continue;
}
continue er for ikke at skulle beregne en position til vores dot i samme frame - det kan
SAGTENS vente til næste frame. så er vi tilbage til vores antialias dot. Som vi husker,
skal vi bruge komma-delen til at finde intensiteterne, og heltallet til positionen på
skærmen.
Husk at vores newstar funktion skal tilpasse til at gi en vinkel mellem 0 og 360 (0 incl.)
- ellers er resten som vi kender det (ja - xpos er jo så bare radius istedet for :).
Nu mangler vi bare en perspektiv beregning. Der findes en masse toturials om hvordan
"3D to 2D projection" virker. Jeg bruger en ekstra variabel:
#define MAXRAD 3500
float rad = (18100/(MAXRAD-star[n].radius));
og vores positions beregning bliver igen lavet om til :
float x = cos(star[n].angel*RAD)*rad+HALFRESX;
float y = cos(star[n].angel*RAD)*rad+HALFRESY;
Nu ser mit loop nogen lunde sådan her ud:
for(int n=0;n
if((x<0) || (y<0) ||
(x>=(FRESX-1.0)) || (y>=(FRESY-1.0)))
{
newstar(&star[n],NEWVALUE);
continue;
}
star[n].angel+=.25;
float bn= star[n].brightness;
int index = (int) (x+y*FRESX);
Screen[index] = (int) ((1.0-fx) * (1.0-fy) * bn);
Screen[index+1] = (int) (fx * (1.0-fy) * bn);
Screen[index+RESX] = (int) ((1.0-fx) * fy * bn);
Screen[index+RESX+1]= (int) (fx * fy * bn);
}
Det kan optimeres helt overdrevet, og gøres pænere etc etc.. men lige nu skal vi bare
have det til at virke - så kan vi altid optimerer alle mulige steder - det vigtigste er,
man forstår hvad der sker.
Allerede nu begynder antialias at hjælpe på resultatet, og vi er ikke engang nået til
et 3D starfield endnu.
Der er een ting man KUNNE lave (hvis man gad) - som ville hjælpe endnu mere. Vores dots
kan stadig blinke, og det er fordi vi laver noget man kalder overdraw. Det betyder at
næsten sorte dots kan blive tegner efter nogle meget klare dots, og dermed overtegne dem.
Resultater er at de forsvinder i nogle frames. Derfor vil vi "blend" vores
pixels sammen, hvor vi binder vores værdier til en rækkevidde fra 0 til 255. Der hvor vi
vil tegne en del at vores dot, henter vi værdien først, ligger sammen med vores værdi
vi vil tegne, og sørger for at vores resultat ikke overflow'er en byte (altså bliver
størrer end 255).
Så mangler vi efterhånden bare et 3D starfield.
Selve strukturen er næsten som en 2D starfield.
typedef struct
{
float xpos, ypos, zpos, brightness;
}STAR3D;
Ved 3D starfields er flytter man bare ens stjerner i object-space (x, y eller z
aksen). Hastigheden skal bare være den samme for alle ens dots, da perspektiv beregningen
sørger for at dybden. Selve rotationen er nok nemmest at beregne med en 3*3 matrix. (4*4
bør ikke være nødvendig, med mindre man vil "svæve" mellem sine dots via et
camera, eller forbundet med en verden.)
Selve 3D teknikken vil jeg ikke forklarer - der er så mange godt artikler om dette, og
emnet hører heller ikke hjemme her (plus det er et stort område :).
Man skal forestille sig en kasse, hvor alle ens dots MÅ være inden for. f.eks :
min val, max val
x -100 , 100
y -100 , 100
z -100 , 100
så "wrapper" stjerne bare rund - altså hvis man bevæger dem via x-aksen og
xpos bliver størrer end 100, trækker men 200 fra xpos.
En kølig ting man nu kan udbygge med, er sprites. i stedet for pixels sætter vi strites
ind, af f.eks små stjerner (jeg er bare SÅ kreativ). Disse burde skaleres efter deres
z-værdier, så dem der er langt væk er små, og dem der er tæt på er (tada) store. Her
kommer andre sjove ting ind som 2D-clip. Her bliver sortering for alvor nødvendigt
(hvilket man også bruger z til). Man kunne også blend sine stjerner ind - og bruge
animerede stjerner for at gøre det total poppet :) - feel free - explore.
Jeg kunne hurtigt forestille mig animerede flares svæve rund og blende sammen i
forskellige farver :)
Alt dette kan vi se på i en anden tut...
Det var hvad jeg lige havde at sige om starfields - Håber det kunne bruges. End of part 1. Der mangler stadig nogle ting for at lave en "rigitg" rutine. Her skal vi bruge shading, rigtig 3D og andet... men det kommer i part 2