Hallo, dit is het vierde deel van onze serie over 3D grafische engines. Deze keer zullen we behandelen rasteren: het proces van het nemen van een vorm die wordt beschreven door wiskundige formules en het converteren naar een afbeelding op uw scherm.
Tip: Alle concepten in dit artikel zijn opgebouwd uit klassen die we hebben vastgesteld in de eerste drie berichten, dus zorg ervoor dat u ze eerst bekijkt.
Hier volgt een overzicht van de lessen die we tot nu toe hebben gemaakt:
Puntklasse variabelen: num tuple [3]; // (x, y, z) Operators: Point AddVectorToPoint (Vector); Point SubtractVectorFromPoint (Vector); Vector SubtractPointFromPoint (punt); Null SetPointToPoint (punt); Functies: drawPoint; // teken een punt op de positie tuple Vector Class Variables: num tuple [3]; // (x, y, z) Operators: Vector AddVectorToVector (Vector); Vector SubtractVectorFromVector (Vector); Vector RotateXY (graden); Vector RotateYZ (graden); Vector RotateXZ (graden); Vectorschaal (s0, s1, s2); // ontvangt een schaling 3-tuple, retourneert de geschaalde vector Cameraklasse Vars: int minX, maxX; intIJp, maxY; int minZ, maxZ; array objectsInWorld; // een array van alle bestaande objecten Functies: null drawScene (); // tekent alle benodigde objecten op het scherm
Je kunt het voorbeeldprogramma uit het derde deel van de serie bekijken om te zien hoe ze samenwerken.
Laten we nu een paar nieuwe dingen bekijken!
rasteren (of rasteren, als je wilt) is het proces van het nemen van een vorm die beschreven is in een vectorgrafiekformaat (of in ons geval, wiskundig) en het om te zetten in een rasterafbeelding (waarbij de vorm past op een pixelstructuur).
Omdat wiskunde niet altijd zo precies is als we het nodig hebben voor computergraphics, moeten we algoritmen gebruiken om de vormen die het beschrijft op ons gehele scherm te krijgen. Bijvoorbeeld, een punt kan vallen op de coördinaat \ ((3.2, 4.6) \) in de wiskunde, maar als we het renderen, moeten we het naar \ ((3, 5) \) duwen zodat het in de pixelstructuur van ons scherm.
Elk type vorm dat we rasteren, heeft zijn eigen algoritme om dit te doen. Laten we beginnen met een van de eenvoudigere vormen om te rasteren: de lijnstuk.
Lijnsegmenten zijn een van de eenvoudigste vormen die kunnen worden getekend en zijn daarom vaak een van de eerste dingen die in elke geometrieklasse worden behandeld. Ze worden voorgesteld door twee verschillende punten (een beginpunt en een eindpunt) en de lijn die de twee verbindt. Het meest gebruikte algoritme bij het rasteren van een lijnsegment wordt genoemd Bresenham's algoritme.
Stap voor stap werkt het algoritme van Bresenham als volgt:
sx
, sy
, en fout bij het vangen van eigenschappen (ik zal de wiskundige definitie hieronder weergeven).Voordat we het algoritme van Bresenham implementeren, kunnen we een basislijnsegmentklasse samenstellen voor gebruik in onze engine:
LineSegment Class Variables: int startX, startY; // het startpunt van ons lijnsegment int endX, endY; // het eindpunt van ons lijnsegment Functie: array returnPointsInSegment; // alle punten liggend op dit lijnsegment
Als je een transformatie wilt uitvoeren op ons nieuwe Lijnstuk
klasse, alles wat je hoeft te doen is je gekozen transformatie toe te passen op de begin- en eindpunten van de Lijnstuk
en plaats ze dan terug in de klas. Alle punten die er tussenin vallen, worden verwerkt wanneer het Lijnstuk
zelf is getekend, aangezien het algoritme van Bresenham alleen de begin- en eindpunten vereist om elk volgend punt te vinden.
Om voor de Lijnstuk
Klasse die bij onze huidige motor past, we kunnen eigenlijk geen trek()
functie ingebouwd in de klas, daarom heb ik gekozen voor het gebruik van een returnPointsInSegment
functioneer in plaats daarvan. Deze functie retourneert een array van elk punt dat zich binnen het lijnsegment bevindt, zodat we het lijnsegment eenvoudig kunnen tekenen en ruimen, indien van toepassing.
Onze functie returnPointsInSegment ()
ziet er een beetje als volgt uit (in JavaScript):
function returnPointsInSegment () // maak een lijst om alle punten van het lijnsegment op te slaan in var pointArray = new Array (); // stel de variabelen van deze functie in op basis van de begin- en eindpunten van de klasse var x0 = this.startX; var y0 = this.startY; var x1 = this.endX; var y1 = this.endY; // definieer vectorverschillen en andere variabelen die vereist zijn voor het algoritme van Bresenham var dx = Math.abs (x1-x0); var dy = Math.abs (y1-y0); var sx = (x0 & x1)? 1: -1; // step x var sy = (y0 & y1)? 1: -1; // step y var err = dx-dy; // haal de initiële foutwaarde // stel het eerste punt in de array pointArray.push in (nieuw punt (x0, y0)); // Hoofdverwerkingslus terwijl (! ((X0 == x1) && (y0 == y1))) var e2 = err * 2; // de foutwaarde behouden // gebruik de foutwaarde om te bepalen of het punt naar boven of beneden moet worden afgerond als (e2 => -dy) err - = dy; x0 + = sx; if (e2 < dx) err += dx; y0 += sy; //add the new point to the array pointArray.push(new Point(x0, y0)); return pointArray;
De eenvoudigste manier om de weergave van onze lijnsegmenten toe te voegen aan onze cameraklasse is om een eenvoudige toe te voegen als
structuur, vergelijkbaar met het volgende:
// doorloop array van objecten als (class type == Point) // onze huidige rendercode else if (class type == LineSegment) var segmentArray = LineSegment.returnPointsInSegment (); // doorluspunten in de array, tekenen en ruimen zoals we eerder hebben gedaan
Dat is alles wat u nodig heeft om uw eerste vormklasse in gebruik te nemen! Als je meer wilt weten over de meer technische aspecten van Bresenham's algoritme (met name de foutgedeelten), kun je het Wikipedia-artikel erover lezen.
Het rasteren van een cirkel is een beetje moeilijker dan het rasteren van een lijnsegment, maar niet veel. We zullen de gebruiken middelpunt cirkel algoritme om al ons zware werk te doen, wat een uitbreiding is van het eerder genoemde Bresenham's algoritme. Als zodanig volgt het dezelfde stappen als die hierboven werden genoemd, met enkele kleine verschillen.
Ons nieuwe algoritme werkt als volgt:
Onze cirkelklasse zal sterk lijken op onze lijnsegmentklasse, en ziet er ongeveer zo uit:
Circle Class Variabelen: int centerX, centerY; // het middelpunt van onze cirkel int radius; // de straal van onze cirkel Functie: array returnPointsInCircle; // alle punten binnen deze cirkel
Onze returnPointsInCircle ()
functie gaat zich gedragen op dezelfde manier als onze Lijnstuk
De functie van de klasse doet dat, een reeks punten retourneren zodat onze camera ze naar behoefte kan renderen en eventueel kan laten verdwijnen. Hierdoor kan onze motor verschillende vormen aannemen, met slechts minimale wijzigingen die voor elke motor nodig zijn.
Dit is wat onze returnPointsInCircle ()
functie zal er uitzien (in JavaScript):
function returnPointsInCircle () // bewaar alle punten van de cirkel in een array var pointArray = new Array (); // stel de waarden in die nodig zijn voor het algoritme var f = 1 - radius; // gebruikt om de voortgang van de getekende cirkel te volgen (sinds de semi-recursieve) var ddFx = 1; // step x var ddFy = -2 * this.radius; // stap y var x = 0; var y = this.radius; // dit algoritme houdt geen rekening met de verste punten, // dus moeten we ze handmatig instellen pointArray.push (nieuw punt (this.centerX, this.centerY + this.radius)); pointArray.push (nieuw punt (this.centerX, this.centerY - this.radius)); pointArray.push (nieuw punt (this.centerX + this.radius, this.centerY)); pointArray.push (nieuw punt (this.centerX - this.radius, this.centerY)); while (x < y) if(f >= 0) y--; ddFy + = 2; f + = ddFy; x ++; ddFx + = 2; f + = ddFx; // bouw onze huidige arc pointArray.push (nieuw Point (x0 + x, y0 + y)); pointArray.push (nieuw punt (x0 - x, y0 + y)); pointArray.push (nieuw punt (x0 + x, y0 - y)); pointArray.push (nieuw punt (x0 - x, y0 - y)); pointArray.push (nieuw punt (x0 + y, y0 + x)); pointArray.push (nieuw punt (x0 - y, y0 + x)); pointArray.push (nieuw punt (x0 + y, y0 - x)); pointArray.push (nieuw punt (x0 - y, y0 - x)); return pointArray;
Nu voegen we er gewoon een toe als
verklaring voor onze belangrijkste tekenreeks, en deze cirkels zijn volledig geïntegreerd!
Hier ziet u hoe de bijgewerkte tekenreeks eruit kan zien:
// doorloop array van objecten als (klassetype == punt) // onze huidige weergavecode else if (klassetype == LineSegment) var segmentArray = LineSegment.returnPointsInSegment (); // doorluspunten in de array, tekenen en ruimen zoals we eerder hebben gedaan else if (class type == Circle) var circleArray = Circle.returnPointsInCircle (); // doorluspunten in de array, tekenen en ruimen zoals we eerder hebben gedaan
Nu we onze nieuwe lessen uit de weg hebben geruimd, laten we iets maken!
Ons programma zal dit keer simpel zijn. Wanneer de gebruiker op het scherm klikt, gaan we een cirkel tekenen waarvan het middelpunt het punt is waarop is geklikt en waarvan de straal een willekeurig getal is.
Laten we de code eens bekijken:
main // setup voor je favoriete grafische API hier // setup voor toetsenbordinvoer (is mogelijk niet nodig) var camera = new Camera (); // maak een instantie van de cameraklasse camera.objectsInWorld []; // maak 100 objectruimten binnen de array van de camera // stel de camera in voor de beeldruimte camera.minX = 0; camera.maxX = schermbreedte; camera.minY = 0; camera.maxY = schermhoogte; camera.minZ = 0; camera.maxZ = 100; while (key! = esc) if (mouseClick) // maak een nieuwe cirkel camera.objectenInWorld.push (nieuwe Circle (mouse.x, mouse.y, random (3,10)); // render alles in de scene camera.drawScene ();
Met een beetje geluk zou je nu je bijgewerkte engine kunnen gebruiken om een aantal geweldige cirkels te tekenen.
Nu we enkele basisfuncties voor rastering in onze engine hebben, kunnen we eindelijk beginnen met het tekenen van enkele nuttige dingen op ons scherm! Niets dat te ingewikkeld is, maar als je dat wilde, kon je wat stokfiguurtjes samenstellen, of iets van dien aard.
!