Förstå Affine Transformations With Matrix Mathematics

Inspirerad av professor Wildberger i sin föreläsningsserie om linjär algebra, tänker jag genomföra sina matematiska idéer med Flash. Vi ska inte gräva in matrisens matematiska manipulation genom linjär algebra: bara genom vektorer. Denna förståelse, trots att den sparar elegansen av linjär algebra, räcker för att starta oss in i några intressanta möjligheter till 2x2 matrismanipulation. I synnerhet använder vi den för att applicera olika skjuvning, skevning, flipping och skalningseffekter till bilder vid körning.


Slutresultatförhandsvisning

Låt oss ta en titt på det slutliga resultatet vi kommer att arbeta för. Tryck på de fyra riktknapparna - upp, ner, vänster, höger - för att se några effekter vi kan uppnå med affine-omvandlingar.

Om du bara använder vänster och höger piltangent, verkar fisken simma runt i ett pseudo-3D isometriskt utrymme.


Steg 1: Olika samordningsområden

Grafik ritas på koordinatutrymmen. För att kunna manipulera dem, speciellt för att översätta, rotera, skala, reflektera och skryta grafik, är det viktigt att vi förstår koordinatutrymmen. Vi brukar använda inte bara en, men flera koordinatutrymmen i ett enda projekt - det här gäller inte bara för designers som använder Flash IDE, men också för programmerare som skriver ActionScript.

I Flash IDE händer detta när du konverterar dina ritningar till MovieClip-symboler: varje symbol har sitt eget ursprung.

Bilden ovan visar steget koordinatutrymme (röd punkt) och symbolens koordinatutrymme (registreringspunkt markerad med crosshair). För att veta vilket utrymme du befinner dig för närvarande, observera fältet under tidslinjen för Flash IDE som visas av bilden nedan.

(Jag använder Flash CS3, så dess plats kan skilja sig från CS4 och CS5.) Det jag vill betona är att det finns olika koordinatutrymmen och det faktum att du redan är bekant med att använda dem.


Steg 2: Bakgrunden

Nu finns det en bra anledning till detta. Vi kan använda ett koordinatutrymme som en referens för att ändra det andra koordinatutrymmet. Det här låter alien, så jag har tagit med Flash-presentationen nedan för att underlätta min förklaring. Klicka och dra de röda pilarna. Spela runt med det.

I bakgrunden är ett blått rutnät, och i förgrunden är ett rött rutnät. De blå och röda pilarna justeras initialt längs x- och y-axeln i Flash-koordinatutrymmet, vars centrum jag har flyttat till mitten av scenen. Det blå nätet är ett referensnät; rutnätet ändras inte när du interagerar med de röda pilarna. Det röda rutnet kan å andra sidan omorienteras och skalas genom att dra de röda pilarna.

Observera att pilarna också indikerar en viktig egenskap hos dessa nät. De indikerar begreppet en enhet x och en enhet y på deras respektive rutnät. Det finns två röda pilar på det röda rutnätet. Var och en av dem anger längden på en enhet på x-axeln och y-axeln. De dikterar också orienteringen av koordinatutrymmet. Låt oss ta den röda pilen som pekar längs x-axeln och förläng den till dubbelt så lång som den ursprungliga pilen (visas i blått). Observera följande bilder.

Vi ser att bilden (den gröna lådan) som ritats på det röda gallret nu sträcker sig horisontellt på grund av det faktum att det här röda galleret är ritat på är nu dubbelt så stort. Den punkt jag försöker göra är ganska enkel: du kan använda ett koordinatutrymme som grund för att ändra ett annat koordinatutrymme.


Steg 3: Affinera koordinera rymden

Så vad är ett "affine koordinatutrymme"? Jo, jag är säker på att du är noggrann nog att observera att dessa koordinatutrymmen är ritade med parallella rutor. Låt oss ta det röda affineutrymmet till exempel: det finns ingen garanti för att både x-axeln och y-axeln alltid är vinkelräta mot varandra, men försäkra dig om att du försöker att tweak pilarna, men du kommer aldrig att komma till ett sådant fall som nedan.


Detta koordinatutrymme är inte ett affine koordinatutrymme.

I själva verket hänvisar x- och y-axlar vanligtvis till det kartesiska koordinatutrymmet, såsom visas nedan.

Observera att de horisontella och vertikala rutorna är vinkelräta mot varandra. Cartesian är en typ av affine koordinat utrymme, men vi kan förvandla det till andra affine utrymmen som vi föredrar. De horisontella och vertikala rutorna behöver inte nödvändigtvis vara vinkelräta mot varandra.


Exempel på ett affine-koordinatutrymme
Ett annat exempel på ett koordinatutrymme

Steg 4: Affinera transformationer

Som du kanske har gissat är affine-omvandlingarna översättning, skalning, reflektion, skevning och rotation.


Original affisch utrymme
Skalat affine utrymme
Reflekterat affine utrymme
Skewed affine space
Roterat och skalat affine utrymme

Självklart, fysiska egenskaper som x, y, scaleX, scaleY och rotation beror på utrymmet. När vi ringer till dessa egenskaper, förvandlar vi faktiskt affinekoordinater.


Steg 5: Förstå Matrix

Jag hoppas att bilderna som visas ovan är tydliga nog för att driva idén. Detta beror på att för en programmör som arbetar med FlashDevelop, ser vi inte de nät som Flash IDE visar bekvämt för designers. Alla dessa måste leva i ditt huvud.

Förutom att föreställa sig dessa nät, måste vi också utnyttja hjälp av Matris klass. Således är det viktigt att ha en matematisk förståelse av matriser, så vi ska granska matrisens verksamhet här: addition och multiplikation.


Steg 6: Geometrisk Betydelse av Matrix Addition

Matrix-operationer innebär geometrisk geometri. Med andra ord kan du se vad de betyder på ett diagram. Låt oss anta att vi har fyra punkter i vårt koordinatutrymme och skulle vilja flytta dem till en rad nya platser. Detta kan göras med matristillägg. Kolla in bilden nedan.

Som ni kan se, flyttar vi faktiskt hela det lokala koordinatutrymmet (röda rutor) där dessa fyra punkter ritas. Notationen för att utföra dessa operationer är som visas nedan:

Vi kan också se att detta skift faktiskt kan representeras med en vektor av (tx, ty). Låt oss differentiera vektorer och statiska punkter i koordinatutrymmen genom användning av parenteser och fälthakparenteser. Jag har omskrivit dem på bilden nedan.


Steg 7: Implementering av ActionScript

Här är en enkel implementering av matristillägget. Kolla in kommentarerna:

 public class Addition utökar Sprite public function Addition () var m: Matrix = ny Matrix (); // instantiate matris m.tx = stage.stageWidth * 0,5; // skift i x m.ty = stage.stageHeight * 0,5; // skift i y var d: DottedBox = new DottedBox (); // skapa den anpassade grafiken (prickad rutan är en Sprite) addChild (d); d.transform.matrix = m; // tillämpa matrisen på vår grafik

Steg 8: Geometrisk Betydelse av Matrix Multiplikation

Matrixmultiplicering är något mer sofistikerad än matristillägg men Prof Wildberger har elegant brutit ner den till denna enkla tolkning. Jag ska ödmjukt försöka att upprepa hans förklaring. För dem som vill dyka djupare in i förståelsen för linjär algebra som leder till detta, kolla professorns föreläsningsserie.

Låt oss börja med att ta itu med fallet med identitetsmatrisen, I.

Från bilden ovan vet vi att multiplicera en godtycklig matris, A, med identitetsmatrisen, jag, kommer alltid att producera A. Här är en analogi: 6 x 1 = 6; identitetsmatrisen liknar numret 1 i den multiplikationen.

Alternativt kan vi skriva resultatet i följande vektorformat vilket i hög grad kommer att förenkla vår tolkning:

Den geometriska tolkningen av denna formel visas i bilden nedan.

Från kartesiska rutnätet (vänster rutnät) kan vi se den blå punkten ligger vid (2, 1). Om vi ​​nu skulle transformera detta ursprungliga rutnät av x och y till ett nytt rutnät (höger rutnät) enligt en uppsättning vektorer (under höger rutnät) flyttas den blå punkten till (2, 1) på det nya rutnätet - men när vi kartlägger det här tillbaka till det ursprungliga nätet, är det samma punkt som tidigare.

Eftersom vi förvandlar det ursprungliga gallret till ett annat galler som delar samma vektorer för x och y, ser vi ingen skillnad. Faktum är att ändringarna av x och y i denna transformation är noll. Detta är vad det menade med identitetsmatris, ur en geometrisk synvinkel.

Men om vi försöker utföra en kartläggning med andra omvandlingar, så kommer vi se någon skillnad. Jag vet att detta inte var det mest avslöjande exemplet att börja med, så låt oss gå vidare till ett annat exempel.


Steg 9: Scaling längs X

Bilden ovan visar en skala av koordinatutrymmet. Kolla in vektorn av x i transformerat koordinatutrymme: en enhet av den transformerade x står för två enheter i original x. På det transformerade koordinatutrymmet är koordinaten för den blå punkten fortfarande (2, 1). Om du försöker kartlägga denna koordinat från det transformerade nätet till det ursprungliga nätet är det dock (4, 1).

Hela ideen fångas av bilden ovan. Vad sägs om formeln? Resultatet ska vara konsekvent; Låt oss kolla upp det.

Jag är säker på att du kommer ihåg dessa formler. Nu har jag lagt till deras respektive betydelser.

Nu kolla in det numeriska resultatet av vårt skaleringsexempel.

  • Ursprunglig koordinat: (2, 1)
  • Vektor på transformerad x-axel: (2, 0)
  • Vektor på transformerad y-axel: (0, 1)
  • Förväntat resultat: (2 * 2 + 0 * 1, 0 * 2 + 1 * 1) = (4, 1)

De håller med varandra! Nu kan vi gärna tillämpa denna idé på andra omvandlingar. Men före det, en ActionScript-implementering.


Steg 10: Implementering av ActionScript

Kolla in ActionScript-implementeringen (och den resulterande SWF-filen) nedan. Observera att en av de överlappande rutorna sträcker sig längs x med en skala av 2. Jag har markerat viktiga värden. Dessa värden kommer att tweaked i de senare stegen för att representera olika transformationer.

 public class Multiplication utökar Sprite public function Multiplication () varref: DottedBox = new DottedBox (); // skapa referensgrafik addChild (ref); ref.x = stadium.stageWidth * 0,5; ref.y = stage.stageHeight * 0,5; var m: Matrix = ny matris (); // instantiate matris m.tx = stage.stageWidth * 0,5; // skift i x m.ty = stage.stageHeight * 0,5; // skift i y m.a = 2; m.c = 0; m.b = 0; m.d = 1; var d: DottedBox = ny DottedBox (); // skapa den anpassade grafiska addChild (d); d.transform.matrix = m // använd matrisen på vår grafik

Steg 11: Skalning X och Y

Här har vi skalat gallret med en faktor två längs både x- och y-axlarna. Den blå punkten är vid (2, 1) i det ursprungliga gallret före transformationen och (4, 2) i det ursprungliga gallret efter omvandlingen. (Självklart är det fortfarande (2, 1) i ny rutnät efter omvandlingen.)

Och för att bekräfta resultatet numeriskt ...

... de matchar igen! För att se detta i ActionScript-implementeringen, ändra bara värdet på m.d från 1 till 2.

(Observera att sträckningsriktningen från y är nedåt, inte uppåt, eftersom y ökar nedåt i Flash men uppåt i det normala kartesiska koordinatutrymmet som jag använde i diagrammet.)


Steg 12: Reflektion

Här har vi reflekterat gallret längs x-axeln med hjälp av dessa två vektorer, så positionen för den blå punkten i det ursprungliga gallret ändras från (2, 1) till (-2, 1). Den numeriska beräkningen är enligt följande:

ActionScript-implementeringen är densamma som tidigare men använder istället dessa värden: m.a = -1, m.b = 0 att representera vektorn för x-transformationen och m.c = 0 och m. d = 1 att representera vektorn för y-transformationen.

Därefter reflekterar vi samtidigt på x och y? Kolla in bilden nedan.

Beräknas också numeriskt i bilden nedan.

För ActionScript-implementering ... ja, jag är säker på att du vet vilka värden som ska sättas in i matrisen. m.a = -1, m.b = 0 att representera vektorn för x-transformationen; m.c = 0 och m. d = -1 att representera vektorn för y-transformationen. Jag har inkluderat den slutliga SWF nedan.


Steg 13: Skewing och skärning

Skewing kommer med lite kul. För fallet med bilden nedan har det transformerade gallret fått sin x-axel omorienterad och skalad. Jämför de röda pilarna i båda rutorna nedan: de är olika, men y-axeln förblir oförändrad.


skevning

Visuellt verkar det som att förvrängning händer längs y-riktningen. Detta är sant eftersom vår transformerade x-axel nu har en y-komponent i sin vektor.

Numera är det här som händer ...

När det gäller genomförandet har jag listat tweaksna nedan.

  • m.a = 2
  • m.b = 1
  • m.c = 0
  • m.d = 1

Jag är säker på att du nu vill prova saker själv, så fortsätt och tweak

  • orienteringen av transformerad y-axel samtidigt som x-axeln bibehålls
  • orienteringen av båda axlarna helt och hållet

Jag har inkluderat Flash-utgången för båda fallen enligt nedan. För läsare som vill ha lite hjälp med dessa värden, kolla in Multiplication_final.as i källans nedladdning.


Steg 14: Rotation

Jag anser att rotation är en delmängd av skevning. Den enda skillnaden är att i rotation hålls storleken på en enhet av både x och y-axeln, liksom vinkelnheten mellan de två axlarna.

ActionScript tillhandahåller faktiskt en metod i Matris klass, rotera(), att göra detta. Men låt oss gå igenom det ändå.

Nu vill vi inte ändra storleken på en längd i x och y från det ursprungliga gallret. bara för att ändra orienteringen av varje. Vi kan använda trigonometri för att komma fram till resultatet som visas i bilden ovan. Med en vridningsvinkel a får vi det önskade resultatet med hjälp av vektorer av (cos a, sin a) för x-axeln och (-in i a, cos a) för y-axeln. Storleken för varje ny axel är fortfarande en enhet, men varje axel kommer att ligga i en vinkel jämfört med originalet.

För ActionScript-implementering, förutsatt att vinkeln, a, är 45 grader (det vill säga 0,25 * Pi radianer), justera just matrisvärdena till följande:

 var a: Nummer = 0,25 * Math.PI m.a = Math.cos (a); m.c = -1 * Math.sin (a); m.b = Math.sin (a); m.d = Math.cos (a);

Den fullständiga källan kan hänvisas till i Multiplication_final.as.


Steg 15: Ansökan

Att ha en vektorinterpretation av en 2x2-matris ger utrymme för oss att utforska. Dess tillämpning vid manipulering av bitmappar (BitmapData, LineBitmapStyle, LineGradientStyle, etc.) är utbredd - men jag tror att jag ska spara det för en annan handledning. I fallet med den här artikeln ska vi försöka skryta vår sprite vid körtid så att det ser ut som om det faktiskt bläddrar i 3D.


Vy över en pseudo-3D isometrisk värld

Från bilden ovan kan vi se att i en värld med isometrisk vy håller någon grafisk "stående" sin y-axelvektor oförändrad medan x-axelvektorn roterar. Observera att en längdenhet för x- och y-axeln inte förändras - med andra ord, ingen skalning ska hända i någon axel, bara rotera runt x-axeln.

Här är ett exempel på denna idé i Flash. Klicka någonstans på scenen och börja dra runt för att se fisken skeva. Släpp för att stoppa din interaktion.

Här är den viktiga delen av ActionScript. Jag har markerat de avgörande linjerna som hanterar x-axelrotationen. Du kan också referera till FakeIso.as.

 privat var f1: fisk, m: matris; privat var disp: punkt; privat varaxel: Punkt, axelJ: Punkt; allmän funktion FakeIso () disp = new Point (stage.stageWidth * 0.5, stage.stageHeight * 0.5); m = ny matris (); m.tx = disp.x; m.ty = disp.y; // förskjutas till mitten av scenen f1 = ny fisk (); addChild (f1); f1.transform.matrix = m; // tillämpa omvandling till fisk axel = ny punkt (1, 0); // vektor för x-axeln axel = ny punkt (0, 1); // vektor för y-axelstadiet.addEventListener (MouseEvent.MOUSE_DOWN, start); // start interaktionsstadiet.addEventListener (MouseEvent.MOUSE_UP, slutet); // slutändring privatfunktionsstart (e: MouseEvent): void f1.addEventListener (Event.ENTER_FRAME, uppdatering);  privatfunktionens slut (e: MouseEvent): void f1.removeEventListener (Event.ENTER_FRAME, uppdatering);  privatfunktionsuppdatering (e: Event): void axisX.setTo (mouseX - f1.x, mouseY - f1.y); // bestämma orienteringen (men storleken ändras också) axelax.normalize (1); // fixa storleken på vektor med ny orientering till 1 enhet apply2Matrix (); // tillämpa matris på fisk privat funktion apply2Matrix (): void m.setTo (axelax.x, axelax.y, axelx.x, axel.y, disp.x, disp.y); f1.transform.matrix = m; 

Här har jag använt Point klassen för att lagra vektorer.


Steg 16: Lägg till tangentbordskontroll

I detta steg ska vi försöka lägga till tangentbordskontroller. Fiskeplatsen uppdateras enligt hastigheten, velo. Vi definierar stegvisa steg för positivt (medurs) rotation och negativ (moturs) rotation också.

 velo = ny punkt (1, 0); // velo kommer att användas för att definiera x-axeln axel = ny punkt (0, 1); delta_positive = ny matris (); delta_positive.rotate (Math.PI * 0,01); // positiv rotation delta_negativ = ny matris (); delta_negative.rotate (Math.PI * -0.01); // negativ rotation

Vid en knapptryckning, velo kommer att rotera:

 privat funktion keyUp (e: KeyboardEvent): void if (e.keyCode == Keyboard.LEFT) velo = delta_negative.transformPoint (velo) // rotera velo moturs annars om (e.keyCode == Keyboard.RIGHT ) velo = delta_positive.transformPoint (velo) // rotera väl medurs

Nu för varje ram ska vi försöka att färga framsidan av fisken och sneda fisken också. Om hastigheten, velo, har en storleksgrad av mer än 1 och vi applicerar den på fiskens matris, m, vi kommer också att få en skaleringseffekt - så för att eliminera denna möjlighet ska vi normalisera hastigheten och sedan bara tillämpa den på fiskens matris.

 privat funktion uppdatering (e: händelse): void var front_side: Boolean = velo.x> 0 // kontrollerar fiskens framsida om (front_side) f1.colorBody (0x002233,0.5) // färg framsidan av fisken annars f1.colorBody (0xFFFFFF, 0,5) // vit applicerad på baksidan av fisken disp = disp.add (velo); // uppdatera nuvarande förskjutning med hastighet var velo_norm: Punkt = velo.clone (); // i fall velo> 0, måste vi räkna om 1 enhets längd för x. velo_norm.normalize (1); // notera att x-axeln mer än 1 ska utföra skalning. Vi vill inte ha det för nu m.setTo (velo_norm.x, velo_norm.y, axisY.x, axelY.y, disp.x, disp.y); f1.transform.matrix = m; 

Steg 17: Din fisk

Klicka på scenen och tryck sedan på vänster och höger piltangent för att se att fisken ändrar riktningen.


Steg 18: En annan tangentbordskontroll

För att krydda upp saker, låt oss också tillåta kontrollen av y-axelvektorn.

 privat funktion keyUp (e: KeyboardEvent): void if (e.keyCode == Keyboard.LEFT) velo = delta_negative.transformPoint (velo) annars om (e.keyCode == Keyboard.RIGHT) velo = delta_positive.transformPoint vel om (e.keyCode == Keyboard.UP) axisY = delta_negative.transformPoint (axisY) annars om (e.keyCode == Keyboard.DOWN) axisY = delta_positive.transformPoint (axisY)

För att bestämma fiskens framsida måste vi nu införliva y-axeln i. Här är koden för det:

 var front_side: Boolean = velo.x * axel.y> 0 om (front_side) f1.colorBody (0x002233,0.5) annars f1.colorBody (0xFFFFFF, 0,5)

Steg 19: Din icke-så-regelbundna fisk

Tja, för vissa kan resultatet av att kontrollera båda axlarna visa sig vara lite förvirrande, men meningen är att du nu kan skjuta din fisk, översätta den, reflektera den och rotera den! Prova kombinationerna av upp + vänster, upp + höger, ner + vänster, ner + höger.

Se också om du kan behålla den främre sidan av fisken (fisken blir grå). Tips: Tryck upp fortlöpande, sedan vänster, sedan neråt, och höger. Du gör en rotation!

Slutsats

Jag hoppas att du hittar matrismatematik en värdefull tillgång till dina projekt efter att ha läst den här artikeln. Jag hoppas kunna skriva lite mer på applikationer av 2x2-matrisen i lite snabba tips som förgrenar sig från den här artikeln och vidare Matrix3D vilket är nödvändigt för 3D-manipulationer. Tack för läsningen, terima kasih.