Låt oss bygga en 3D-grafikmotor Linjära transformationer

Välkommen till den andra delen av vår 3D Graphics Engine-serie! Den här gången ska vi prata om linjära transformationer, vilket gör att vi kan ändra egenskaper som rotation och skalning av våra vektorer och titta på hur man applicerar dem till de klasser vi redan har byggt.

Om du inte redan har läst den första delen av denna serie föreslår jag att du gör det nu. Bara om du inte kommer ihåg, här är en snabb omgång av vad vi skapade förra gången:

Point Class Variables: num tuple [3]; // (x, y, z) Operatörer: Peka AddVectorToPoint (Vector); Punkt SubtraherVectorFromPoint (Vector); SubtractPointFromPoint (Point); Funktioner: // rita en punkt i sin position tuple med din favorit grafik API drawPoint;  Vektor klass Variabler: num tuple [3]; // (x, y, z) Operatörer: Vector AddVectorToVector (Vector); Vector SubtraherVectorFromVector (Vector); 

De två klasserna kommer att ligga till grund för hela vår grafikmotor, där den första representerar en punkt (en fysisk plats i ditt utrymme) och den andra representerar en vektor (rymden / rörelsen mellan två punkter).

För vår diskussion om linjära transformationer borde du göra en liten ändring i Point-klassen: istället för att mata ut data till en konsollinje som tidigare använder du ditt favoritgrafik API och har funktionen ritar nuvarande punkt till skärmen.


Grundar av linjära omvandlingar

Bara en varning: Linjära transformationsekvationer ser mycket värre ut än de faktiskt är. Det kommer att innebära en viss trigonometri, men du behöver inte faktiskt veta på vilket sätt att göra det trigonometri: Jag kommer att förklara vad du måste ge varje funktion och vad du kommer att få ut, och för mellanliggande saker kan du bara använda någon räknare eller mattebibliotek som du kan ha.

Tips: Om du vill ha bättre förståelse för de inre verkningarna av dessa ekvationer, bör du titta på den här videon och läsa den här PDF-filen.

Alla linjära transformationer tar denna form:

\ [B = F (A) \]

Detta anger att om du har en linjär transformationsfunktion \ (F () \), och din ingång är vektorn \ (A \), så kommer din produktion att vara vektorn \ (B \).

Var och en av dessa delar - de två vektorerna och funktionen - kan representeras som en matris: vektorn \ (B \) som en 1x3-matris, vektorn \ (A \) som en annan 1x3-matris och den linjära transformationen \ (F \) som en 3x3-matris (a transformationsmatris).

Det betyder att när du expanderar ekvationen ser det ut så här:

\ [
\ Begin bmatrix
b_ 0 \\
b_ 1 \\
B_ 2
\ End bmatrix
=
\ Begin bmatrix
f_ 00 & f_ 01 & f_ 02 \\
f_ 10 & f_ 11 & f_ 12 \\
f_ 20 & f_ 21 & f_ 22
\ End bmatrix
\ Begin bmatrix
a_ 0 \\
a_ 1 \\
a_ 2
\ End bmatrix
\]

Om du någonsin har tagit en klass i trigonometri eller linjär algebra börjar du förmodligen komma ihåg den mardröm som var matrismatematik. Lyckligtvis finns det ett enklare sätt att skriva ut denna ekvation för att ta det mesta av besväret ur det. Det ser ut så här:

\ [
\ Begin bmatrix
B_ 0 \\
B_ 1 \\
B_ 2
\ End bmatrix
=
\ Begin bmatrix
f_ 00 a_ 0 + f_ 01 a_ 1 + f_ 02 a_ 2 \\
f_ 10 a_ 0 + f_ 11 a_ 1 + f_ 12 a_ 2 \\
f_ 20 a_ 0 + f_ 21 a_ 1 + f_ 22 a_ 2 \\
\ End bmatrix
\]

Emellertid kan dessa ekvationer ändras genom att ha en andra ingång, såsom i fallet med rotationer, där en vektor och dess rotationsmängd båda måste ges. Låt oss ta en titt på hur rotationer fungerar.


rotationer

En rotation är per definition en cirkelrörelse av ett objekt runt en rotationspunkt. Rotationspunkten för vårt utrymme kan vara en av tre möjligheter: antingen XY-planet, XZ-planet eller YZ-planet (där varje plan består av två av våra grundvektorer som vi diskuterade i första delen av serien ).

Våra tre rotationspunkter betyder att vi har tre separata rotationsmatriser, enligt följande:

XY-rotationsmatris:
\ [
\ Begin bmatrix
cos \ theta & -sin \ theta & 0 \\
synd \ theta & cos \ theta & 0 \\
0 & 0 & 1 \\
\ End bmatrix
\]

XZ-rotationsmatris:

\ [
\ Begin bmatrix
cos \ theta & 0 & sin \ theta \\
0 & 1 & 0 \\
-sin \ theta & 0 & cos \ theta
\ End bmatrix
\]

YZ rotationsmatris:

\ [
\ Begin bmatrix
1 & 0 & 0 \\
0 & cos \ theta & -sin \ theta \\
0 & sin \ theta & cos \ theta
\ End bmatrix
\]

Så att rotera en punkt \ (A \) runt XY-planet med 90 grader (\ (\ pi / 2 \) radianer - de flesta mattebibliotek har en funktion för att konvertera grader till radianer), skulle du följa dessa steg:

\ [
\ Begin linje
\ Begin bmatrix
B_ 0 \\
B_ 1 \\
B_ 2
\ End bmatrix
& =
\ Begin bmatrix
cos \ frac \ pi 2 & -sin \ frac \ pi 2 & 0 \\
synd \ frac \ pi 2 & cos \ frac \ pi 2 & 0 \\
0 & 0 & 1
\ End bmatrix
\ Begin bmatrix
a_ 0 \\
a_ 1 \\
a_ 2
\ End bmatrix \\
& =
\ Begin bmatrix
cos \ frac \ pi 2 a_ 0 + -in \ frac \ pi 2 a_ 1 + 0a_ 2 \\
sin \ frac \ pi 2 a_ 0 + cos \ frac \ pi 2 a_ 1 + 0a_ 2 \\
0a_ 0 + 0a_ 1 + 1a_ 2
\ End bmatrix \\
& =
\ Begin bmatrix
0a_ 0 + -1a_ 1 + 0a_ 2 \\
1a_ 0 + 0a_ 1 + 0a_ 2 \\
0a_ 0 + 0a_ 1 + 1a_ 2
\ End bmatrix \\
& =
\ Begin bmatrix
-a_ 1 \\
a_ 0 \\
a_ 2
\ End bmatrix
\ End linje
\]

Så om din initiala punkt \ (A \) var \ ((3,4,5) \) skulle din outputpunkt \ (B \) vara \ ((- 4,3,5) \).

Övning: Rotationsfunktioner

Som en övning, försök skapa tre nya funktioner för Vektor klass. Man borde rotera vektorn runt XY-planet, en runt YZ-planet och en runt XZ-planet. Dina funktioner ska få önskad mängd grader för rotation som en inmatning och returnera en vektor som en utgång.

Grundflödet av dina funktioner ska vara enligt följande:

  1. Skapa utgångsvektor.
  2. Konvertera graderingången till radianform.
  3. Lös för varje bit av utgångsvektorerna tupel med hjälp av ekvationerna ovan.
  4. Returnera utgångsvektorn.

skalning

Skalning är en transformation som antingen förstorar eller minskar ett objekt baserat på en uppsättning skala.

Att utföra denna transformation är ganska enkel (åtminstone jämfört med rotationer). En skalningstransformation kräver två ingångar: an inmatningsvektor och a skalning 3-tupel, som definierar hur inmatningsvektorn ska skalas i förhållande till var och en av rymdens grundaxlar.  

Exempelvis representerar scaling tuplen \ ((s_ 0, s_ 1, s_ 2) \), \ (s_ 0 \) skalan längs X-axeln, \ (s_ 1 \) längs Y-axeln och \ (s_ 2 \) längs Z-axeln.

Skalningsomvandlingsmatrisen är som följer (där \ (s_ 0 \), \ (s_ 1 \) och \ (s_ 2 \) är elementen i skalning 3-tupeln):

\ [
\ Begin bmatrix
s0 & 0 & 0 \\
0 & s1 & 0 \\
0 & 0 & s2
\ End bmatrix
\]

För att göra inmatningsvektorn A \ ((a_ 0, a_ 1, a_ 2) \) dubbelt så stor längs X-axeln (det vill säga med användning av en skalering 3-tuple \ (S = ( 2, 1, 1) \)), skulle matematiken se ut så här:

\ [
\ Begin linje
\ Begin bmatrix
B_ 0 \\
B_ 1 \\
B_ 2
\ End bmatrix
& =
\ Begin bmatrix
s0 & 0 & 0 \\
0 & s1 & 0 \\
0 & 0 & s2
\ End bmatrix
\ Begin bmatrix
a_ 0 \\
a_ 1 \\
a_ 2
\ End bmatrix \\
& =
\ Begin bmatrix
2 & 0 & 0 \\
0 & 1 & 0 \\
0 & 0 & 1
\ End bmatrix
\ Begin bmatrix
a_ 0 \\
a_ 1 \\
a_ 2
\ End bmatrix \\
& =
\ Begin bmatrix
2a_ 0 + 0a_ 1 + 0a_ 2 \\
0a_ 0 + 1a_ 1 + 0a_ 2 \\
0a_ 0 + 0a_ 1 + 1a_ 2
\ End bmatrix \\
& =
\ Begin bmatrix
2a_ 0 \\
a_ 1 \\
a_ 2
\ End bmatrix
\ End linje
\]

Så om du gav ingångsvektorn \ (A = (3,4,0) \), skulle din output vektor \ (B \) vara \ ((6,4,0) \).

Övning: Skalningsfunktioner

Som en annan övning, lägg till en ny funktion till din vektorklass för skalning. Den här nya funktionen ska ta in en skalering 3-tupel och returnera en utgångsvektor.  

Grundflödet av dina funktioner ska vara enligt följande:

  1. Skapa utgångsvektor.
  2. Lös för varje bit av utgångsvektorerna tupel med hjälp av ekvationen ovan (som kan förenklas till y0 = x0 * s0; y1 = x1 * s1; y2 = x2 * s2).
  3. Returnera utgångsvektorn.

Låt oss bygga något!

Nu när du har linjära transformationer under ditt bälte, låt oss bygga ett snabbt litet program för att visa upp dina nya färdigheter. Vi ska göra ett program som drar en grupp punkter till skärmen och låter oss sedan modifiera dem som helhet genom att utföra linjära transformationer på dem.  

Innan vi börjar, vill vi också lägga till en annan funktion till vår Punkt klass. Detta kommer att kallas setPointToPoint (), och kommer helt enkelt att ställa in aktuell punkts position till den punkt som skickas till den. Den kommer att få en poäng som en inmatning, och kommer inte att returnera någonting.

Här är några snabba specifikationer för vårt program:

  • Programmet kommer att hålla 100 poäng i en array.
  • När D tangenten trycks ned, kommer programmet att torka den aktuella skärmen och dra tillbaka punkterna.
  • När en tangenten trycks in, kommer programmet att skala alla punkters platser med 0,5.
  • När S tangenten trycks in, kommer programmet att skala alla punkters platser med 2,0.
  • När R tangenten trycks in, kommer programmet att rotera alla punkternas läge med 15 grader på XY-planet.
  • När Fly tangenten trycks in, kommer programmet att gå ut (om du inte gör det med JavaScript eller ett annat webbinriktat språk).

Våra nuvarande klasser:

Point Class Variables: num tuple [3]; // (x, y, z) Operatörer: Peka AddVectorToPoint (Vector); Punkt SubtraherVectorFromPoint (Vector); Vector SubtractPointFromPoint (Punkt); // anger aktuell punkts position till den för den inmatade punkten Null SetPointToPoint (Point); Funktioner: // rita en punkt i sin position tuple med din favorit grafik API drawPoint;  Vektor klass Variabler: num tuple [3]; // (x, y, z) Operatörer: Vector AddVectorToVector (Vector); Vector SubtraherVectorFromVector (Vector); Vector RotateXY (grader); Vector RotateYZ (grader); Vector RotateXZ (grader); Vektorskala (s0, s1, s2); 

Med dessa specifikationer, låt oss titta på vad vår kod kan vara:

main // setup för ditt favoritgrafik API här // setup for keyboard input (kanske inte krävs) här // skapa en array med 100 poäng Point Array pointArray [100]; för (int x = 0; x < pointArray.length; x++)  //Set its location to a random point on the screen pointArray[x].tuple = [random(0,screenWidth), random(0,screenHeight), random(0,desiredDepth));  //this function clears the screen and then draws all of the points function redrawScreen()  //use your Graphics API's clear screen function ClearTheScreen();   for (int x = 0; x < pointArray.length; x++)  //draw the current point to the screen pointArray[x].drawPoint();   // while the escape is not being pressed, carry out the main loop while (esc != pressed)  // perform various actions based on which key is pressed if (key('d') == pressed)  redrawScreen();  if (key('a') == pressed)  //create the space's origin as a point Point origin = new Point(0,0,0); Vector tempVector; for (int x = 0; x < pointArray.length; x++)  //store the current vector address for the point, and set the point tempVector = pointArray[x].subtractPointFromPoint(origin); //reset the point so that the scaled vector can be added pointArray[x].setPointToPoint(origin); //scale the vector and set the point to its new, scaled location pointArray[x].addVectorToPoint(tempVector.scale(0.5,0.5,0.5));  redrawScreen();  if(key('s') == pressed)  //create the space's origin as a point Point origin = new Point(0,0,0); Vector tempVector; for (int x = 0; x < pointArray.length; x++)  //store the current vector address for the point, and set the point tempVector = pointArray[x].subtractPointFromPoint(origin); //reset the point so that the scaled vector can be added pointArray[x].setPointToPoint(origin); //scale the vector and set the point to its new, scaled location pointArray[x].addVectorToPoint(tempVector.scale(2.0,2.0,2.0));  redrawScreen();  if(key('r') == pressed)  //create the space's origin as a point Point origin = new Point(0,0,0); Vector tempVector; for (int x = 0; x < pointArray.length; x++)  //store the current vector address for the point, and set the point tempVector = pointArray[x].subtractPointFromPoint(origin); //reset the point so that the scaled vector can be added pointArray[x].setPointToPoint(origin); //scale the vector and set the point to its new, scaled location pointArray[x].addVectorToPoint(tempVector.rotateXY(15));  redrawScreen();   

Nu borde du ha ett coolt, litet program för att visa upp alla dina nya tekniker! Du kan kolla in min enkla demo här.


Slutsats

Medan vi verkligen inte täckte alla möjliga linjära transformationer som är tillgängliga, börjar vår mikromotor ta form. 

Som alltid finns det några saker som lämnades ut ur vår motor för enkelhet (nämligen skjuvning och reflektioner i denna del). Om du vill veta mer om de två typerna av linjära transformationer kan du läsa mer om dem på Wikipedia och dess relaterade länkar.

I nästa del av denna serie kommer vi att täcka olika visningsutrymmen och hur man kan kasta objekt som ligger utanför vår åsikt.

Om du behöver extra hjälp, gå vidare till Envato Studio, där du kan hitta massor av fantastiska 3D Design & Modeling-tjänster. Dessa erfarna leverantörer kan hjälpa dig med ett brett utbud av olika projekt, så bläddra bara till leverantörerna, läs recensioner och betyg och välj den rätta personen som hjälper dig. 

3D Design & Modeling tjänster på Envato Studio