                          - SEPTIC'S DEMOSKOLA -

                                Lektion 11
                                ¯¯¯¯¯¯¯¯¯¯
                        Skriven av Vicious / Septic

                                 7 Jan 94



                                 Inledning
                                 ¯¯¯¯¯¯¯¯¯
Välkommen till årets första lektion i Demoskolan! Nu börjar vi äntligen
komma in på de intressanta delarna, tycker ni inte det? Idag ska jag
nämligen förklara tredimensionell vektor-rotation som när det kom för några
år sen skapade en revolution i demovärlden och som ännu idag används
flitigt i demos.
  Om den här texten ter sig svår eller rentav omöjlig att begripa ska ni
inte bli oroliga. Det kan ta ett tag (och några genomläsningar) om man ska
fullt förstå hur det hela fungerar, och man har mycket nytta av att ha läst
matte på gymnasiet i ett par år.
  Men, nu kör vi igång istället för att snacka...


                                   Teori
                                   ¯¯¯¯¯
När man pratar om vektorer inom demovärlden menar man oftast ett "vektor-
objekt", dvs ett objekt uppbygggt av vektorer. En vektor är egentligen
(matematiskt sett) en visare med viss bestämd längd som pekar på en punkt.
Visaren kan beskrivas genom att man anger längd och vinkel, och det kan se
ut ungefär så här:

     9|
     8|
     7|
     6|
     5|
     4|   _
     3|   /|
     2|  /
     1| /
     0|/)a_________________
       0 1 2 3 4 5 6 7 8 9 

Den här vektorn pekar på en punkt som har koordinaterna 2;4. Om vektorn
skulle beskrivas normalt skulle man säga att den hade längden 4.47 och
vinkeln 63.4°. Längden får man genom Pythagoras sats här, och vinkeln genom
tangens.
  Såna här vektorer med längd och vinkel är inte vanligt när man gör
vektor-rotationer på datorn (vilket det logiskt sett borde vara, eftersom
det heter VEKTOR-rotation). Istället låter man vektor presenteras med hjälp
av dess ändpunkt, som i vårt fall ligger i punkten 2;4, dvs X-koordinaten
är 2, och Y-koordinaten är 4.

     9|
     8|
     7|
     6|
     5|
     4|    * (2;4)
     3|
     2|
     1|
     0|____________________
       0 1 2 3 4 5 6 7 8 9 

Som ni ser så har vi alltså nu en vanlig punkt i vårt tvådimensionella
koordinatsystem. Om vi nu skulle ta och rotera den runt origo (0;0) så att
den flyttar sig på en cirkulär bana runt nollstället så förstår vi snart
varför vi inte använder en vektors normala representations system med
längd, vinkel. Då skulle vi för varje punkt bli tvungna att ta fram X och Y
höjd med Pythagoras sats, och alltså behöva beräkna två kvadrater och en
kvadratrot för att få fram den nya punktens koordinater.
  Nu kommer istället våra kunskaper i trigonometri till användning! Om vi
utgår från att vår punkt befinner sig i sitt utgångsläge kan vi lätt räkna
om detta utgångsläge till 360 olika vinklar med hjälp av sinus och cosinus.
Låt mig först göra en ändring i koordinatsystemet:

     9|
     8|
     7|
     6|
     5|
     4|
     3|
     2|
     1|
     0|____*_______________
       0 1 2 3 4 5 6 7 8 9 

Hmmmm, vad skulle det där vara bra för? Jag nollställde helt enkelt
Y-koordinaten men behöll X-koordinaten och punkten hamnade således nere på
2;0 istället för 2;4. Jo, snart ska du få se att det hela blir mycket
lättare att tänka ut om man flyttar ner punkten så här, eftersom vi då får
reda på hur lång ut punkten ligger. Vi kan tänka oss att punkten är fast-
bunden med en tråd i origo, och att tråden har längden L. (L blir ju alltså
2 i det här fallet...)
  Vi utgår från att noll grader ligger parallellt med vår X-axel här, och
så säger vi att vi ska rotera punkten till vinkel a. Figuren skulle då se
ut så här ungefär:

     9|
     8|
     7|
     6|
     5|
     4|
     3|
     2|  *
     1| /
     0|/)a_________________
       0 1 2 3 4 5 6 7 8 9 

Eftersom vi vet längden på vektorn (det blir ju i själva verket bara
X-koordinaten när vi satte Y till noll) så ser vi att den nya X-koordinaten
kan beräknas med cosinus:

     2|          *
      |         /|
      |        / |
      |       /  |
      |    L /   | y
     1|     /    |
      |    /     |
      |   /      |
      |  /       |
      | /       _|
     0|/)a_____|_|__________
       0    x    1         2

Längden L är ju, som jag sa, likadan hela tiden, eftersom tråden som vi har
hängt punkten i alltid måste vara lika lång. För att få fram det nya
X-värdet nu använder vi det tigonometriska sambandet som säger att cos
a=närliggande katet dividerat med hypotenusan. Vi får alltså X=L*COS(a).
  Vi kan sen göra nästan samma sak för att räkna ut den nya Y-koordinaten,
genom att använda sambandet sin a=motstående katet dividerat med
hypotenusan. Då får vi Y=L*SIN(a).
  Som jag nämnde innan så är ju längden ekvivalent med X, eftersom vi satte
Y-koordinaten till noll. Alltså kan vi skriva om de två sambanden till:

   X=X*COS(a)
   Y=X*SIN(a)

Nu har vi lärt oss att rotera en punkt som ligger på X-axeln, men oftast är
ju inte fallet så enkelt. Från början hade vi ju vår vektorpunkt på
koordinaten 2;4, så då måste vi göra något åt Y-delen också.
  Vi gör på samma sätt (fast tvärtom :-) och sätter X till noll istället:

     9|
     8|
     7|
     6|
     5|
     4|*
     3|
     2|
     1|
     0|____________________
       0 1 2 3 4 5 6 7 8 9 

Nu blir längden L istället lika med Y, och vår rotationsvinkel a blir a+90°
eftersom Y-axeln ligger 90 grader ifrån X-axeln. Om vi stoppar in detta i
samma koordinatsystem som innan, så får vi de nya formlerna:

   X=Y*COS(90°+a)
   Y=Y*SIN(90°+a)

Vi kan optimera de två uttrycken lite om vi följer några trigonometriska
lagar som säger att COS(90°+a)=-SIN(a) och SIN(90°+a)=COS(a). Om vi ändrar
detta i de två uttrycken får vi:

   X=Y*-SIN(a) <=> X=-Y*SIN(a)
   Y=Y*COS(a)

Sådär...Så långt allt väl! Nu har vi två uttryck för att rotera en punkt
som ligger på X-axeln, och två uttryck för att rotera en punkt som ligger
på Y-axeln. Vill man rotera en punkt som ligger fritt ute i den
tvådimensionella rymden nu, så får man använda sig av alla uttrycken för
att räkna ut den totala komposanten för X och Y. Vad man gör är alltså att
man lägger ihop formlerna för X och formlerna för Y:

   Ny X=X*COS(a)-Y*SIN(a)
   Ny Y=X*SIN(a)+Y*COS(a)

Detta är nu en grundformel för tvådimensionell rotation (vilket är samma
sak som rotation runt Z-axeln i ett tredimensionellt koordinatsystem) och
om vi använder det på vår punkt 2;4 och roterar den till vinkeln 30°, så
får vi följande:

   Ny X=2*COS(30°)-4*SIN(30°)
   Ny Y=2*SIN(30°)+4*COS(30°)

   Ny X=-0.27
   Ny Y=+4.46

Punktens nya koordinat blir alltså -0.27;4.46 när vi roterar den 30 grader.

Men, hur blir det nu med TRE dimensioner istället? Jo, i ett
tredimensionellt koordinatsystem lägger man till en axel, Z-axeln, som
pekar rakt in i skärmen. En punkt i ett sådant system kräver tre stycken
koordinater istället för två. En som beskriver dess position i X-led, en i
Y-led och en i Z-led.

       y
     9|
     8|
     7|
     6|
     5|
     4|    z
     3|   /3
     2|  /2
     1| /1
     0|/0__________________x
       0 1 2 3 4 5 6 7 8 9 

  Den rotation vi gjort tidigare (i två dimensioner) är egentligen en
Z-rotation eftersom vi roterat runt Z-axeln. Vill man rotera runt Y-axeln
istället, så kan man tänka sig att man snurrar sitt koordinat-system så att
Y pekar inåt istället för Z på det här viset:

       z
     9|
     8|
     7|
     6|
     5|
     4|
     3|
     2|
     1|
     0|____________________x
     / 0 1 2 3 4 5 6 7 8 9 
    /1
   /2
  /3
 y

Vad jag har gjort här är alltså bara att Y-axeln har fallit framåt och
Z-axeln rest sig bakom och ställt sig lodrätt upp på bilden. Nu har vi
precis samma koordinatsystem som när vi roterade tvådimensionellt med det
undantaget att Z har ersatt Y.
  Frågan är då nu bara: Kan man strunta i Y koordinaten och rotera Z och X
med samma formler som vi gjorde med X och Y när vi struntade i Z?
  Svaret är JA! Givetvis kan vi det! Vi byter bara ut Y mot Z i våra
formler, och således blir formeln för Y-rotation:

   X=X*COS(a)-Z*SIN(a)
   Z=X*SIN(a)+Z*COS(a)

Mycket enkelt och smidigt, alltså. Här är formlerna för att rotera i X, Y
och Z led:

Rotera runt X-axeln:
-------------------
Z=Z*COS(a)-Y*SIN(a)
Y=Z*SIN(a)+Y*COS(a)

Rotera runt Y-axeln:
-------------------
X=X*COS(a)-Z*SIN(a)
Z=X*SIN(a)+Z*COS(a)

Rotera runt Z-axeln:
-------------------
X=X*COS(a)-Y*SIN(a)
Y=X*SIN(a)+Y*COS(a)

Nu har vi bara ett problem kvar att lösa, nämligen HUR SKA MAN KUNNA VISA
TREDIMENSIONELLA KOORDINATER PÅ EN TVÅDIMENSIONELL SKÄRM?

Tja, det KAN lösas med Pythagoras sats, men eftersom Pythagoras sats skall
undvikas när man vill göras snabba beräkningar (eftersom det innehåller
kvadratrötter och kvadreringar) så måste vi hitta på ett annat sätt.
  Om vi tänker oss att vi har vår punkt en bra bit in i skärmen och bort
från oss, så kan vi tänka oss att det går en tråd från vårt öga ut till
punkten och samtidigt skär skärmens plan. Där tråden skär skärmen blir
alltså punktens riktiga projicerade koordinat.
  Hur ska man nu ta reda på var tråden skär skärmen nu då? Jo, genom
likformighet kan vi utan större problem plocka fram ett enkelt samband.
  Låt oss rita upp en bild av hur det hela kan se ut om vi tänker oss att
vi tittar på vår rymd uppifrån:

                 X-led
     <--------------------------->
                   x1
          ------------------*  «- Vår punkt är här
          |                 /
          |                /
          |               /         /|\
          |              /           |            X1=Punktens X-koordinat
        z |             /            |             Z=Punktens Z-koordinat
          |            /             |             d=Distans till skärmen
          |           /              |  Z-led     X2=Punktens nya X-koord.
          |          /               |
          |   x2    /                |
     _____|________/________         |
          |       /          Skärm   |
          |      /                   |
          |     /                    |
        d |    /                     |
          |   /                      |
          |  /                       |
          | /                       \|/
          |/
          o «-Här befinner sig betraktaren

Om vi tänker oss att den här vyn är ritad uppifrån, så att vi kikar ner på
vår punkt, så ser vi att två likvinkliga trianglar bildats, och alltså är
likformiga.
  Som ni ser har jag lagt in en bokstav D mellan betraktaren och skärmen.
Detta är DISTANSEN. Om vi gör en jämförelse med att titta på en tavla så
ser vi inte så bra om vi tittar ända inpå den. Alltså behöver vi en distans
till skärmen som vi tittar på.
  Vi är alltså intresserade av värdet X2, vilket är punktens aktuella
värdet projicerat på bildskärmen. Likformigheten säger oss att sidan X1
förhåller sig till Z+d som X2 till d:

      X1     X2
     ---- = ----
     Z+d     d

X2 blir alltså:

            X1*d
       X2 = ----
             Z+d

Detta gäller givetvis också för Y-koordinaten. Man byter bara ut X1 och X2
mot Y1 och Y2 i formeln istället.
  För att kunna zooma bilden kan man dock byta ut distansen i täljaren mot
ett DJUP-värde. Formlerna för X och Y ser då ut så här istället:

            X1*DJUP
       X2 = -------
            Z+DIST

            Y1*DJUP
       Y2 = -------
            Z+DIST

Djupet kan man sedan variera för att göra objekt större eller mindre.
Distansen skall dock inte ändras nån gång, utan sätts till ett fast värde.
Z+Distansen får inte vara noll, eftersom det betyder att den projicerade
punkten har distansen noll till ögat, alltså befinner sig inuti ögat, och
det skulle ju vara en fysisk omöjlighet. Dessutom ger det DIVISION by ZERO,
hehe....

  Sådär, det var allt man behöver för att kunna rotera punkter i en
tredimensionell rymd!


                                 Register
                                 ¯¯¯¯¯¯¯¯
Eftersom vektorer är ren och skär matematik finns det inga register att ta
upp idag. Alla beräkningar tas om hand av processorn!


                              Programexemplet
                              ¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯
Vad har vi för något smått och gott här idag då? Jo, en gammal goding från
demos av äldre klass, nämligen en line-vektorkub! Det kanske ni tycker är
surt, men någonstans måste man ju börja när man visar vektorgrafik.
  Okej, ska vi kika på hur programmet funkar nu då...
  Först och främst har vi som vanligt en main-rutin som först väntar på en
speciell rasterposition för att synca vektorn till 50 frames/sek. När
skärmstrålen kommit till rätt ställe flippar vi fram sidan vi ritat ut vår
vektor på med rutinen SwapPages. Sidan vi just visat blir då istället
"ritsida", alltså den sida vi ska rita på.
  Sedan hoppar vi till en helt vanlig clear-rutin som rensar vår ritsida
och sedan den första intressanta rutinen, Rotate.
  Vi hoppar ner och kikar på vad den innehåller direkt...
  Tja, vad har vi här för smått och gott? Jo, först ställer jag in lite
pekare, dels på våra koordinater och en 2d-koordinatplats, plus sinus och
cosinustabellen. 2d-koordinatplatsen behövs eftersom vi inte ska förstöra
våra original-koordinater. I 2d-koordinatplatsen lagras de koordinater som
vi ska använda när vi ritar ut linjerna sedan.
  Efter att jag ställt in pekarna läser jag X, Y och Z koordinaterna i ett
enda svep till D0, D1 och D2. Därefter lagrar jag (temporärt) X-värdet och
hämtar sedan vinkeln för X-rotation. Jag multiplicerar den med 2 och hämtar
sedan cosinus för vinkeln * 16384. Jag multiplicerar detta med Z och sen
gör jag likadant med Y fast med sinus. De två värdena (som är 16384 för
stora) behandlas sedan så att Y dras ifrån Z och sedan skiftar jag med 14
(för att dividera med 16384) och då får vi den nya Z-koordinaten.
  Sedan gör jag på ungefär samma sätt med Y-koordinaten (följer formlen för
X-rotation) och får på så sätt ett nytt Y-värde. De här nya värdena ska
användas sedan i de bägge andra rotationerna som går till på ungefär samma
sätt.
  Efter alla rotationer kommer en liten kodsnutt som konverterar 3d
koordinater till 2d, och den följer exakt formeln som jag bevisade i förra
kapitlet. Först adderar vi en distans till Z, hämtar djup-värdet och
multiplicerar det med X. Sedan dividerar vi med Z+distansen och får det
tvådimensionella X-et smidigt och enkelt. Det fungerar på samma sätt med Y
sedan, så det behöver jag heller inte förklara närmare...
  Om vi hoppar tillbaka och kollar i main-rutinen så ser vi att vi ska
hoppa till en ny spännande rutin kallad DrawCube.
  I DrawCube initierar jag först några adress-register som linjerutinen
kräver. (Detta är samma linjerutin som vi konstruerade förra gången som nu
kommer till nytta :-) Sedan pekar jag på 2d-koordinaterna (vi behöver
alltså inte 3d-koordinaterna här, eftersom linjeritningen inte sker i tre
dimensioner) och på en speciell linjedata-tabell som jag ska förklara
längre ner.
  Vi läser från linjetabellen antal linjer i polygonen för att veta hur
många gånger vi ska loopa vår linjeritningsrutin. Sen subtraherar vi med 2
eftersom den sista linjen alltid ska ritas tillbaka till den första så
behöver den inte vara med i linjedata-tabellen, och för att DBF kräver
loopvärdet-1 för att loopa rätt antal gånger.
  Sedan - i linjeutritnings rutinen som börjar med .DrawPoly - hämtar jag
först ett koordinatnummer från linjedatan. Jag tar det gånger 8 eftersom
det ligger en X och en Y-koordinat som varsitt longword i tabellen. Andra
punkten börjar alltså på start+(2*4), och därav måste man multiplicera
koordinatnummer med 8.
  Efter det hämtar jag X och Y på den positionen vi fick fram och adderar
skärmmitten till dem båda. Detta blir linjens startpunkt.
  Sedan gör jag på samma sätt med linjens slutpunkt, fast observera att jag
INTE flyttar fram linjedata-pekaren när jag läst koordinatnumret! Detta gör
att när vi ritar nästa linje kommer den att hänga ihop med föregående och
vi får en sammanhängande polygon.
  När vi ritat AntalLinjer-1 används den första koordinaten igen för att
kunna rita från den allra sista punkten till den första vilket gör att
polygonen stängs.
  Tillbaks till main-rutinen nu då igen...Här hoppar vi återigen till en
rutin kallad NewAngle.
  Rutinen NewAngle är ganska simpel. Den addera helt enkelt bara rätt
hastighet till rätt vinkel och kollar om vinkeln överstiger 359 grader. Om
den gör det ska den "wrappas" runt så att 360 blir 0, 361 blir 1
osv...Detta åstadkommer man genom att subtrahera 360 från vinkeln.
  Allra sist i main-rutinen har vi en liten kodsnutt (vilket man egentligen
inte ska ha i mainloopen, eftersom koden då lätt blir grötig och
oöverskådlig :-) Den här kodsnutten adderar bara talet 5 till djupet för
att skapa en zoomeffekt när kuben växer. När den nått värdet 360 ska vi
inte addera mer, och då hoppas ADD-instruktionen över...
  Om vi hoppar ner till slutet av sourcen nästan, till en label som heter
Coords, så ser vi kubens koordinater. Dessa är lagrade som X, Y, Z. Första
punkten ligger alltså i -50,-50,-50. Antal punkter är lika med antal hörn
som vårt objekt har. En kub har åtta hörn, och således får man alltså åtta
punkter. Att jag använder negativa tal beror på att jag vill att roterings-
axeln ska ligga i mitten på objektet, eftersom roteringen alltid sker runt
punkten 0,0,0. Om man sätter en punkt längst ut på -50 i X-led, och sedan
en punkt längst ut på 50 i X-led får vi en 100 enheter bred kub, där mitten
ligger på koordinaten 0. Alltså ligger axeln där också, och kuben roterar
runt sin mittpunkt.
  Man kan ändra axeln så att den tex ligger i hörnet istället genom att
addera 50 till alla koordinater. Kuben blir då lika stor, men roterar på
ett helt annat, skojigt sätt.
  Efter koordinatdatan ligger linjedatan. Den ligger lagrad så att först
kommer det ett longword som talar om hur många linjer polygonen består av
och därefter kommer punkternas nummer. På första raden ser vi siffrorna
4,0,1,2,3. Detta betyder att vi ska rita en fyrhörnad polygon. Första
linjen ritas mellan punkt 0 (alltså den första i koordinattabellen) och
punkt 1 (den andra punkten i koordinattabellen). Nästa linje ritas mellan 1
och 2, nästa mellan 2 och 3, och sedan den sista, ritas mellan 3 och 0
(eftersom vi binder ihop polygonen i DrawCube-rutinen ovan, om ni kommer
ihåg det...).
  Vår kub har bara fått fyra sidor här i vår linjedata-tabell med
polygoner, trots att den egentligen har sex sidor. Anledningen till det är
att alla linjer som används av de två återstående sidorna är utritade redan
(eftersom de sammanfaller med de redan utritade polygonernas sidor) så
därför behövde de inte ritas ut.
  Längre ner, efter linjedata-tabellen, ligger sinus och cosinustabellen.
De här tabellerna får ni gärna använda själva hur ni vill. Annars kan nya
tabeller (om man vill multiplicera med ett högre eller lägre värde än
16384, tex för bättre decimal-noggrannhet) skapas i Basic. Det är bara att
generera sinus och cosinusvärden från 0° till 359° (0-2pi) och innan man
lagrar dem i tabellen multiplicera med det värdet man vill använda. Se bara
till att talen avrundas rätt, eftersom en del program, som använder heltal,
inte avrundar korrekt utan bara struntar i decimalerna om man inte använder
ett speciellt kommando som sköter avrundningen.
  Japp, det var nog allt jag behövde berätta om programmet, eftersom det
andra har gåtts igenom i tidigare lektioner.

