Laten we een 3D-grafische engine bouwen lineaire transformaties

Welkom bij het tweede deel van onze 3D Graphics Engine-serie! Deze keer zullen we het hebben over lineaire transformaties, waarmee we eigenschappen zoals de rotatie en schaling van onze vectoren kunnen wijzigen en bekijken hoe we deze kunnen toepassen op de klassen die we al hebben gebouwd.

Als je het eerste deel van deze serie nog niet hebt gelezen, raad ik je aan dit nu te doen. Voor het geval je het je niet herinnert, hier is een korte samenvatting van wat we de vorige keer hebben gemaakt:

Puntklasse variabelen: num tuple [3]; // (x, y, z) Operators: Point AddVectorToPoint (Vector); Point SubtractVectorFromPoint (Vector); SubtractPointFromPoint (Point); Functies: // teken een punt op zijn positie tuple met uw favoriete grafische API drawPoint;  Vectorklasse variabelen: num tuple [3]; // (x, y, z) Operators: Vector AddVectorToVector (Vector); Vector SubtractVectorFromVector (Vector); 

Die twee klassen vormen de basis van onze hele grafische engine, waarbij de eerste een punt vertegenwoordigt (een fysieke locatie in je ruimte) en de tweede een vector vertegenwoordigt (de spatie / beweging tussen twee punten).

Voor onze discussie over lineaire transformaties, moet u een kleine wijziging aanbrengen in de klasse Point: in plaats van gegevens naar een consolelijn te exporteren zoals voorheen, gebruikt u uw favoriete grafische API en laat u de functie het huidige punt naar het scherm tekenen.


Funderingen van lineaire transformaties

Gewoon een waarschuwing: Lineaire transformatie-vergelijkingen zien er veel slechter uit dan ze in werkelijkheid zijn. Er zal wat trigonometrie bij betrokken zijn, maar je hoeft het niet echt te weten hoe om die trigonometrie te doen: ik zal uitleggen wat je elke functie moet geven en wat je eruit krijgt, en voor de tussendoortjes kun je gewoon elke rekenmachine of wiskundebibliotheek gebruiken die je misschien hebt.

Tip: Als je de interne werking van deze vergelijkingen beter wilt begrijpen, moet je deze video bekijken en deze PDF lezen.

Alle lineaire transformaties hebben de volgende vorm:

\ [B = F (A) \]

Dit stelt dat als je een functie voor lineaire transformatie hebt \ (F () \), en je invoer de vector \ (A \) is, dan is je uitvoer de vector \ (B \).

Elk van deze stukken - de twee vectoren en de functie - kan worden weergegeven als een matrix: de vector \ (B \) als een 1x3 matrix, de vector \ (A \) als een andere 1x3 matrix en de lineaire transformatie \ (F \) als een matrix van 3x3 (a transformatiematrix).

Dit betekent dat, wanneer u de vergelijking uitbreidt, dit er als volgt uitziet:

\ [
\ 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
\]

Als je ooit een les in trigonometrie of lineaire algebra hebt gevolgd, begin je je waarschijnlijk de nachtmerrie te herinneren die matrix wiskunde was. Gelukkig is er een eenvoudiger manier om deze vergelijking op te schrijven om de meeste problemen op te lossen. Het ziet er zo uit:

\ [
\ 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
\]

Deze vergelijkingen kunnen echter worden gewijzigd door een tweede invoer te hebben, zoals in het geval van rotaties, waarbij een vector en de rotatiehoeveelheid ervan beide moeten worden gegeven. Laten we eens kijken hoe rotaties werken.


rotaties

Een rotatie is per definitie een cirkelvormige beweging van een object rond een draaipunt. Het punt van rotatie voor onze ruimte kan een van de drie mogelijkheden zijn: ofwel het XY-vlak, het XZ-vlak of het YZ-vlak (waarbij elk vlak bestaat uit twee van onze basisvectoren die we in het eerste deel van de reeks hebben besproken) ).

Onze drie rotatiepunten betekenen dat we als volgt drie afzonderlijke rotatiematrices hebben:

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

XZ rotatiematrix:

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

YZ rotatiematrix:

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

Dus om een ​​punt \ (A \) rond het XY-vlak 90 graden te draaien (\ (\ pi / 2 \) radialen - de meeste wiskundebibliotheken hebben een functie voor het converteren van graden in radialen), zou je deze stappen volgen:

\ [
\ Begin gericht
\ Begin bmatrix
B_ 0 \\
B_ 1 \\
B_ 2
\ End bmatrix
& =
\ Begin bmatrix
cos \ frac \ pi 2 & -sin \ frac \ pi 2 & 0 \\
sin \ 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 + -sin \ 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 gericht
\]

Dus als uw eerste punt \ (A \) \ ((3,4,5) \) was, dan zou uw uitvoerpunt \ (B \) \ ((- 4,3,5) \) zijn.

Oefening: rotatiefuncties

Probeer als oefening drie nieuwe functies voor de Vector klasse. Men moet de vector rond het XY-vlak roteren, één rond het YZ-vlak en één rond het XZ-vlak. Uw functies moeten het gewenste aantal graden voor rotatie als invoer ontvangen en een vector als uitvoer retourneren.

De basisstroom van uw functies zou als volgt moeten zijn:

  1. Creëer uitgangsvector.
  2. Converteer de gradeninvoer in radialen.
  3. Los voor elk deel van de uitgangsvectoren tuple op door de bovenstaande vergelijkingen te gebruiken.
  4. Retourneer de uitvoervector.

scaling

Schalen is een transformatie die een object vergroot of verkleint op basis van een ingestelde schaal.

Het uitvoeren van deze transformatie is vrij eenvoudig (tenminste in vergelijking met rotaties). Een schalingstransformatie vereist twee ingangen: een input vector en een 3-tuple schalen, die definieert hoe de invoervector moet worden geschaald met betrekking tot elk van de basisassen van de ruimte.  

In de schaaltuple \ ((s_ 0, s_ 1, s_ 2) \), staat \ (s_ 0 \) voor de schaalvergroting langs de X-as, \ (s_ 1 \) langs de Y-as, en \ (s_ 2 \) langs de Z-as.

De schalingstransformatiematrix is ​​als volgt (waarbij \ (s_ 0 \), \ (s_ 1 \) en \ (s_ 2 \) de elementen van het 3-tuple schalen zijn):

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

Om de invoervector A \ ((a_ 0, a_ 1, a_ 2) \) twee keer zo groot te maken langs de X-as (dat wil zeggen, een schaal 3-tuple gebruiken \ (S = ( 2, 1, 1) \)), zou de wiskunde er als volgt uitzien:

\ [
\ Begin gericht
\ 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 gericht
\]

Dus als de invoervector \ (A = (3,4,0) \) wordt gegeven, dan is uw uitvoervector \ (B \) \ ((6,4,0) \).

Oefening: schaalfuncties

Als een andere oefening voegt u een nieuwe functie toe aan uw vectorklasse voor schaling. Deze nieuwe functie moet een 3-tullig schaalniveau aannemen en een uitvoervector retourneren.  

De basisstroom van uw functies zou als volgt moeten zijn:

  1. Creëer uitgangsvector.
  2. Los voor elk deel van de uitgangsvectoren tuple op met behulp van de bovenstaande vergelijking (die vereenvoudigd kan worden tot y0 = x0 * s0; y1 = x1 * s1; y2 = x2 * s2).
  3. Retourneer de uitvoervector.

Laten we iets bouwen!

Nu dat je lineaire transformaties onder je riem hebt, laten we een snel klein programma bouwen om te pronken met je nieuwe vaardigheden. We gaan een programma maken dat een groep punten naar het scherm trekt, waarna we ze als geheel kunnen wijzigen door lineaire transformaties op hen uit te voeren.  

Voordat we beginnen, willen we ook nog een functie aan onze toevoegen Punt klasse. Dit wordt gebeld setPointToPoint (), en stelt eenvoudig de positie van het huidige punt in op dat punt dat eraan is doorgegeven. Het ontvangt een punt als invoer en zal niets teruggeven.

Hier zijn enkele snelle specificaties voor ons programma:

  • Het programma bevat 100 punten in een array.
  • Wanneer de D toets wordt ingedrukt, zal het programma het huidige scherm wissen en de punten opnieuw tekenen.
  • Wanneer de EEN toets wordt ingedrukt, zal het programma alle locaties van de punten met 0,5 schalen.
  • Wanneer de S toets wordt ingedrukt, zal het programma alle locaties van de punten met 2.0 schalen.
  • Wanneer de R toets wordt ingedrukt, draait het programma de locatie van alle punten 15 graden rond op het XY-vlak.
  • Wanneer de Ontsnappen toets is ingedrukt, het programma zal afsluiten (tenzij je het maakt met JavaScript of een andere webgerichte taal).

Onze huidige lessen:

Puntklasse variabelen: num tuple [3]; // (x, y, z) Operators: Point AddVectorToPoint (Vector); Point SubtractVectorFromPoint (Vector); Vector SubtractPointFromPoint (punt); // stelt de positie van het huidige punt in op dat van het ingevoerde punt Null SetPointToPoint (Point); Functies: // teken een punt op zijn positie tuple met uw favoriete grafische API drawPoint;  Vectorklasse variabelen: 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); 

Laten we met die specificaties eens kijken naar wat onze code zou kunnen zijn:

main // setup voor uw favoriete grafische API hier // setup voor toetsenbordinvoer (is mogelijk niet vereist) hier // maak een array van 100 punten Point Array pointArray [100]; voor (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 zou je een leuk, klein programma moeten hebben om al je nieuwe technieken te laten zien! Je kunt hier mijn eenvoudige demo bekijken.


Conclusie

Hoewel we zeker niet alle mogelijke lineaire transformatie omvatten die beschikbaar is, begint onze micro-engine vorm te krijgen. 

Zoals altijd zijn er enkele dingen die voor de eenvoud uit onze motor zijn weggelaten (namelijk scheren en reflecties in dit deel). Als je meer wilt weten over deze twee soorten lineaire transformaties, kun je meer over ze te weten komen op Wikipedia en de bijbehorende links.

In het volgende deel van deze serie zullen we verschillende kijkruimtes behandelen en objecten die buiten onze blik vallen, ruimen.

Als u extra hulp nodig hebt, gaat u naar Envato Studio, waar u tal van fantastische 3D-ontwerp- en modelleringservices kunt vinden. Deze ervaren providers kunnen u helpen met een breed scala aan verschillende projecten, dus blader door de providers, lees de recensies en beoordelingen en kies de juiste persoon om u te helpen. 

3D-ontwerp- en modelleringsdiensten op Envato Studio