Starfields i C

Tags:    c++
Skrevet af Bruger #24 @ 19.02.2002
Starfields - part 1


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




Hvad synes du om denne artikel? Giv din mening til kende ved at stemme via pilene til venstre og/eller lægge en kommentar herunder.

Del også gerne artiklen med dine Facebook venner:  

Kommentarer (4)

User
Bruger #2520 @ 25.02.03 22:35
Hej jeg er ked af at give dig en meget lav rating, men det grundes jeg ikke føler der er så meget forklaring på tingene osv. F.eks hvad de diverse ting nu gør.

Det ville være meget rart for mennesker som mig, som ikke har leget med c++ så meget endnu.
User
Bruger #2796 @ 02.03.04 10:15
Det er jo ikke en tutorial om hvordan man bruger c++, men derimod hvordan man laver en starfield. Så jeg synes ikke lige at din begrundelse for hvorfor du synes artiklen er dårlig, holder...

Det var forresten en fin artikel... ;)
User
Bruger #7992 @ 09.11.05 16:47
jeg får 119 fejl når jeg compiler.
kan det være fordi jeg bruger mac?
User
Bruger #8985 @ 17.09.08 20:58
Hvorfor sørensen skriver du ikke, hvilke header-filer du inkluderer? Det må blive en lav 3'er.
Du skal være logget ind for at skrive en kommentar.
t