Hei, dette er den fjerde delen av serien vår på 3D grafikkmotorer. Denne gangen vil vi dekke rastrering: prosessen med å ta en form som beskrives av matematiske formler og konvertere den til et bilde på skjermen.
Tips: Alle konseptene i denne artikkelen er bygget opp av klasser som vi har etablert i de tre første innleggene, så sørg for å sjekke dem ut først.
Her er en gjennomgang av klassene vi har laget så langt:
Point Class Variables: num tuple [3]; // (x, y, z) Operatører: Point AddVectorToPoint (Vector); Punkt SubtraherVectorFromPoint (Vector); Vector SubtractPointFromPoint (punkt); Null SetPointToPoint (punkt); Funksjoner: drawPoint; // tegne et punkt i sin posisjon tuple Vector Class Variables: num tuple [3]; // (x, y, z) Operatører: Vector AddVectorToVector (Vector); Vector SubtractVectorFromVector (Vector); Vector RotateXY (grader); Vector RotateYZ (grader); Vector RotateXZ (grader); Vector Scale (s0, s1, s2); // mottar en skalering 3-tuple, returnerer den skalerte vektoren Kamera klasse Vars: int minX, maxX; int minY, maxY; int minZ, maxZ; array objectsInWorld; // et utvalg av alle eksisterende objekter Funksjoner: null drawScene (); // trekker alle nødvendige objekter til skjermen
Du kan sjekke ut prøveprogrammet fra tredje del av serien for å se hvordan de jobber sammen.
Nå, la oss ta en titt på noen nye ting!
rastrering (eller rasterisation, hvis du liker) er prosessen med å ta en form som er beskrevet i et vektorgrafikkformat (eller i vårt tilfelle matematisk) og konvertere det til et rasterbilde (hvor formen passer på en pikselstruktur).
Fordi matte ikke alltid er så presis som vi trenger det til å være for datagrafikk, må vi bruke algoritmer til å passe til figurene som beskrives på vår heltallsbaserte skjerm. For eksempel kan et punkt falle på koordinat \ ((3.2, 4.6) \) i matematikk, men når vi gjør det, må vi knuse det til \ ((3, 5) \) slik at det kan passe inn i pikselstruktur på skjermen vår.
Hver type form som vi rasteriserer, vil ha sin egen algoritme for å gjøre det. La oss starte med en av de enklere former for rasterisering: linjestykke.
Linjesegmenter er en av de enkleste figurene som kan tegnes, og det er ofte en av de første tingene som er dekket i en hvilken som helst geometriklasse. De er representert av to forskjellige punkter (ett startpunkt og ett sluttpunkt), og linjen som forbinder de to. Den mest brukte algoritmen i rasterisering av et linjesegment kalles Bresenhams algoritme.
Trinn for trinn fungerer Bresenhams algoritme slik:
sx
, sy
, og feilfangende egenskaper (jeg viser den matematiske definisjonen for disse nedenfor).Før vi implementerer Bresenhams algoritme, lar vi sette sammen en grunnlinjesegmentklasse som skal brukes i motoren vår:
LineSegment Class Variabler: int startX, startY; // utgangspunktet for vårt linjesegment int endX, endY; // sluttpunktet til vårt linjesegment Funksjon: array returnPointsInSegment; // alle poeng som ligger på dette linjesegmentet
Hvis du vil utføre en transformasjon på vårt nye Linjestykke
klasse, alt du trenger å gjøre er å bruke din valgte transformasjon til begynnelsen og slutten av poenget Linjestykke
og legg dem tilbake i klassen. Alle punkter som faller mellom vil bli behandlet når Linjestykke
Selve er trukket, som Bresenhams algoritme krever kun begynnelsen og sluttpunktene for å finne hvert etterfølgende punkt.
For å kunne Linjestykke
klasse for å passe med vår nåværende motor, kan vi egentlig ikke ha en tegne()
funksjon innebygd i klassen, derfor har jeg valgt å bruke en returnPointsInSegment
fungere i stedet. Denne funksjonen returnerer en rekke av hvert punkt som eksisterer innenfor linjesegmentet, slik at vi enkelt kan tegne og kaste linjesegmentet etter behov.
Vår funksjon returnPointsInSegment ()
ser litt ut som dette (i JavaScript):
funksjon returnPointsInSegment () // lage en liste for å lagre alle linjesegmentets punkter i var pointArray = new Array (); // sett denne funksjonens variabler basert på klassens start- og sluttpunkter var x0 = this.startX; var y0 = this.startY; var x1 = this.endX; var y1 = this.endY; // definere vektorforskjeller og andre variabler som kreves for Bresenhams algoritme var dx = Math.abs (x1-x0); var dy = Math.abs (y1-y0); var sx = (x0 og x1)? 1: -1; // trinn x var sy = (y0 & y1)? 1: -1; // trinn y var err = dx-dy; // få den opprinnelige feilverdien // sett det første punktet i array pointArray.push (nytt punkt (x0, y0)); // Hovedbehandlingssløyfen mens (! ((X0 == x1) && (y0 == y1))) var e2 = err * 2; // hold feilverdien // bruk feilverdien for å avgjøre om punktet skal avrundes opp eller ned hvis (e2 => -dy) err - = dy; x0 + = sx; hvis (e2 < dx) err += dx; y0 += sy; //add the new point to the array pointArray.push(new Point(x0, y0)); return pointArray;
Den enkleste måten å legge til gjengivelsen av våre linjesegmenter i kameraklassen vår er å legge til i en enkel hvis
struktur, ligner på følgende:
// loop gjennom rekke objekter hvis (klassetype == Punkt) // gjør gjeldende gjeldende kode annet hvis (klassetype == Linjesegment) var segmentArray = LineSegment.returnPointsInSegment (); // loop gjennom punkter i matrisen, tegne og kaste dem som vi tidligere har hatt
Det er alt du trenger for å få din første formeklasse oppe! Hvis du vil lære mer om de mer tekniske aspektene ved Bresenhams algoritme (spesielt feildelene), kan du sjekke Wikipedia-artikkelen på det.
Rasterizing en sirkel er litt vanskeligere enn å rasterisere et linjesegment, men ikke mye. Vi skal bruke midtpunktsirkelalgoritme å gjøre all vår tunge løft, som er en forlengelse av den tidligere nevnte Bresenhams algoritme. Som sådan følger det lignende trinn for de som ble oppført ovenfor, med noen mindre forskjeller.
Vår nye algoritme fungerer slik:
Vår sirkelklasse vil være veldig lik vår linjesegmentklasse, ser noe ut som dette:
Sirkelklasse Variabler: int centerX, centerY; // midtpunktet av vår sirkel int radius; // radius av vår sirkel Funksjon: array returnPointsInCircle; // alle poeng i denne sirkelen
Våre returnPointsInCircle ()
funksjonen skal oppføre seg på samme måte som vår Linjestykke
klassens funksjon gjør det, og returnerer en rekke punkter slik at kameraet vårt kan gjengi og kaste dem etter behov. Dette lar vår motor håndtere en rekke former, med bare mindre endringer som trengs for hver.
Her er hva vår returnPointsInCircle ()
funksjonen vil se ut (i JavaScript):
funksjon returnPointsInCircle () // lagre alle sirkelens poeng i en array var pointArray = new Array (); // sette opp verdiene som trengs for algoritmen var f = 1 - radius; // brukes til å spore fremdriften av den trekkede sirkelen (siden den semi-rekursive) var ddFx = 1; // trinn x var ddFy = -2 * this.radius; // trinn y var x = 0; var y = this.radius; // denne algoritmen tar ikke hensyn til de lengste punktene, // så vi må sette dem manuelt pointArray.push (nytt punkt (this.centerX, this.centerY + this.radius)); pointArray.push (nytt punkt (this.centerX, this.centerY - this.radius)); pointArray.push (nytt punkt (this.centerX + this.radius, this.centerY)); pointArray.push (nytt punkt (this.centerX - this.radius, this.centerY)); mens (x < y) if(f >= 0) y--; ddFy + = 2; f + = ddFy; x ++; ddFx + = 2; f + = ddFx; // bygge vår nåværende bue punktArray.push (nytt punkt (x0 + x, y0 + y)); pointArray.push (nytt punkt (x0 - x, y0 + y)); pointArray.push (nytt punkt (x0 + x, y0 - y)); pointArray.push (nytt punkt (x0 - x, y0 - y)); pointArray.push (nytt punkt (x0 + y, y0 + x)); pointArray.push (nytt punkt (x0 - y, y0 + x)); pointArray.push (nytt punkt (x0 + y, y0 - x)); pointArray.push (nytt punkt (x0 - y, y0 - x)); returpunktArray;
Nå legger vi bare til en annen hvis
erklæring til vår hovedtrekksløyfe, og disse kretsene er fullt integrert!
Slik ser den oppdaterte tegningsløkken ut:
// loop gjennom rekke objekter hvis (klassetype == punkt) // gjør vår gjeldende renderingskode annet hvis (klassetype == LineSegment) var segmentArray = LineSegment.returnPointsInSegment (); // loop gjennom punkter i arrayet, tegne og kaste dem som vi har tidligere annet hvis (klassetype == sirkel) var circleArray = Circle.returnPointsInCircle (); // loop gjennom punkter i matrisen, tegne og kaste dem som vi tidligere har hatt
Nå som vi har våre nye klasser ute av veien, la oss lage noe!
Vårt program kommer til å være enkelt denne gangen. Når brukeren klikker på skjermen, skal vi tegne en sirkel hvis senterpunkt er punktet som ble klikket, og hvis radius er et tilfeldig tall.
La oss ta en titt på koden:
main // setup for din favoritt grafikk-API her // oppsett for tastaturinngang (kanskje ikke nødvendig) her var kamera = nytt kamera (); // lage en forekomst av kameraklassen camera.objectsInWorld []; // lage 100 objekt mellomrom i kameraets array // sett kameraets visning space camera.minX = 0; camera.maxX = screenWidth; kamera.minY = 0; camera.maxY = screenHeight; camera.minZ = 0; camera.maxZ = 100; mens (key! = esc) if (mouseClick) // opprett en ny sirkel camera.objectsInWorld.push (ny sirkel (mouse.x, mouse.y, random (3,10)); // gjeng alt scene kamera.drawScene ();
Med noe hell, bør du nå kunne bruke din oppdaterte motor for å tegne noen flotte sirkler.
Nå som vi har noen grunnleggende rasteriseringsfunksjoner i vår motor, kan vi endelig begynne å tegne noen nyttige ting på skjermen vår! Ingenting er for komplisert enda ennå, men hvis du ville, kunne du kutte sammen noen pinnefigurer eller noe av det slag.
!