Grundlagenforschung:
Bitmap-Rotation in 3D
|
|
Home
Workshop-Seite
Shockwave-Beispiel
Download
als zip-Archiv
|
Die Rückkehr der Mathematik...
Um in Director die Illusion
einer dreidimensionalen Bewegung zu erzeugen, sind mathematische
Berechnungen nötig, die Bildschirmkoordninaten in einen dreidimensionalen
Raum transferieren, dort eintsprechend manipulieren und dann wieder
in Bildschirmkoordinaten umwandeln. Zugegebenermaßen ist bei
mir der Mathe-Leistungskurs schon einige Zeit her, und Lingo war
mir bisher nicht der Ort für mathematische Spielereien. Daß
sich das nun ändern könnte, liegt an QUADs. Director 7
erlaubt es, die Eckpunkte von Bitmap- (Text-, AnimGif-) Darstellern
unanbhängig voneinander zu manipulieren; und dieses einfache
Feature ist die Grundlage für all jene optischen Täuschungen,
die wir heute als director-eigenes 3-D zu sehen bekommen.
Wir müssen also einen
Weg finden, die Eckpunkte der Bitmap so zu plazieren, daß
der __Eindruck__ einer räumlichen Verzerrung entsteht. Mathematisch
läßt sich dies auf unterschiedliche Weise lösen:
schauen Sie sich mal die Beispiele von Arnauld
de la Grandière oder von Che
Tamahori an, die schon in Director 6 (also ohne Quads, nur mit
Linien) 3-D-Transformationen vorgenommen haben.
Der Weg, den ich hier
vorstellen möchte, scheint mir ein recht universeller, wenn
auch mathematisch nicht ganz anspruchsloser zu sein: Matrix-Transformationen.
Sie können natürlich
die entsprechenden Funktionen einfach nutzen, ohne sich um die dahinterliegende
Mathematik zu kümmern. Dennoch ist ein Blick hinter die Kulissen
auch für Nicht-Mathematiker (wie mich) durchaus spannend...
|


 |
|
|
3-D-Spinner
Das folgende Behavior
rotiert eine Bitmap um die senkrechte und waagrechte Achse und kann
zusätzlich eine Bewegung durch die Tiefe des Raumes simulieren
(entsprechend einer Verschiebung und Skalierung der Bitmap).
Ein weiteres kleines Behavior,
das auch im Beispiel-Shockwavefilm zum Einsatz kommt, erlaubt es,
das 3-D-Spinner-Behavior zu konfigurieren bzw. in Echtzeit zu steuern.
Es ist hier aus Gründen der Übersichtlichkeit aus dem
Hauptbehavior ausgeliedert.
Die Grundstruktur
des 3-D-Spinner-Behaviors wird im beginsprite
und prepareframe-Handler
festgelegt. Hier werden die Hilfsfunktionen aufgerufen, die die
mathematischen Transformationen vornehmen. Im beginSprite-Handler
sind dies GetIdentityMat()
und InitQuadOffsets();
außerdem werden dort die Dauer der Rotation in X- und in Y-Richtung,
die für die Perspektivenberechnung wichtige Property myViewDistance
sowie mit do_xrot
und do_yrot
zwei Properties festgelegt, die es später erlauben werden,
die Bewegung auf eine Rotationsachse einzuschränken.
Im prepareframe-Handler
finden alle Matrixtransformationen TransformMatrix()
und die Rückübersetzung auf zweidimensionale Bühnenkoordinaten
DrawQuad()
statt.
-- 3-D-Spinner
-- by David Calaprice / John Dowdell (Macromedia)
-- vereinfacht und angepaßt von Joachim Gola
property spriteNum
property myOrigOffsets, myQuadOffsets, myMatrix
property myXPeriod, myYPeriod, myViewDistance
property do_xrot, do_yrot
on beginSprite me
myMatrix = GetIdentityMat()
InitQuadOffsets(me)
myXPeriod = 3 * 60 -- 3 Sekunden für x-Rot.
myYPeriod = 3 * 60 -- 3 Sekunden für y-Rot.
myViewDistance = 500 -- magic
do_xrot = 1 -- x-Rotation Ein/Aus
do_yrot = 1 -- y-Rotation Ein/Aus
end
on prepareFrame me
TransformMatrix(me)
DrawQuad(me)
end
|


 |
|
|
Die Funktion InitQuadOffsets
setzt das Quad des Bitmapsprites, das ja in der Form [point(103.0000,
63.0000), point(410.0000, 63.0000), point(410.0000, 279.0000), point(103.0000,
279.0000)] vorliegt, in eine 4x4-Matrix um. Die Koordinaten werden
dabei auf den loc of sprite bezogen. Heraus kommt eine Matrix in
der Form: [[-153.0000, -108.0000, 0, 1], [154.0000, -108.0000, 0,
1], [154.0000, 108.0000, 0, 1], [-153.0000, 108.0000, 0, 1]], oder,
zwecks besserer Lesbarkeit:
[[-153.0000, -108.0000, 0, 1],
[154.0000, -108.0000, 0, 1],
[154.0000, 108.0000, 0, 1],
[-153.0000, 108.0000, 0, 1]]
Die ersten drei Werte entsprechen damit x-, y- und z-Werten relativ
zum "Nullpunkt", d.h. hier zum Mittelpunkt des Sprites.
Diese Matrix beschreibt die zweidimensionale Gestalt der
Bitmap der y-Wert ist denn auch 0. Sie beschriebt diese Gestalt
aber in einer Form, die dann im DrawQuad()-Handler
in den dreidimensionalen Raum transformiert werden kann.
on InitQuadOffsets
me
--
Quad-Werte der Bitmap in eine Matrix umwandeln
-- und so Startwerte für myQuadOffset setzen
theQuad = sprite(spriteNum).quad
theLoc = sprite(spriteNum).loc
myOrigOffsets = []
repeat with i = 1 to 4
offsetX = theQuad[i][1] - theLoc[1]
offsetY = theQuad[i][2] - theLoc[2]
add myOrigOffsets, [offsetX, offsetY, 0,
1]
end repeat
myQuadOffsets = duplicate(myOrigOffsets)
end
|


 |
|
|
Die Property myQuadOffsets
enthält nun bei jedem prepareFrame die gerade aktuellen Quad-Positionen
der Bitmap, beschrieben als Matrix. Später wird diese Punkt-Matrix
mit einer Translation-Matrix multipliziert, die die Punkte in einen
rechtwinkligen (orthagonalen, also noch nicht perspektivischen)
3-dimensionalen Raum überführt und dort manipuliert. Die
Erstellung dieser Translation-Matrix führt in die Niederungen
der Matrix-Algebra. Machen Sie sich für die folgenden Ausführungen
klar, daß sie sich auf einen theoretischen Raum beziehen,
der zum jetzigen Zeitpunkt noch keine Beziehung zum zweidimensionalen
Raum der Bühne hat.
Die Funktion TransformMatrix()
erstellt die Translation-Matrix. 2*pi()
ist dabei eine ganze Umdrehung; ausgehend von myXPeriod
und myYPeriod,
die wir im beginSprite-Handler
gesetzt haben, wird zunächst der zeitabhängige Anteil
des Gesamtwinkels 2*pi() als xAngle
und yAngle
berechnet. Danach wird's spannend...
on TransformMatrix
me
-- 2*pi() ist eine ganze Umdrehung
-- xAngle ist der zeitabhängige Anteil an der
-- ganzen Umdrehung (myXPeriod) als Winkel-
-- angabe in Radians
xAngle = do_xrot * 2 * pi() * (the ticks mod integer(myXPeriod))
/¬ float(myXPeriod)
yAngle = do_yrot * 2 * pi() * (the ticks mod integer(myYPeriod))
/¬
float(myYperiod)
myMatrix = GetIdentityMat()
myMatrix = RotateBy (myMatrix, #x, xAngle)
myMatrix = RotateBy (myMatrix, #y, yAngle)
end
|


 |
|
|
Berechnung
der Translationsmatrix:
Identität
oder die Nulltransformation wird in folgender Matrix ausgedrückt:
1 0 0 0
0 1 0 0
0 0 1 0
0 0 0 1
Die Funktion GetIdentityMat()
stellt eine solche Matrix zur Verfügung. Die ersten 3x3 Felder
beziehen sich dabei auf die x-, y- und z-Werte des dreidimensionalen
Raumes. Die Rotationen um die einzeln Achsen modifizieren jeweils
Werte an bestimmten Stellen der Grund-Matrix (hier mit X
markiert). Die Rotation um die Z-Achse führen wir hier nur
der Vollständigkeit halber an:
Rot. um x-Achse Rot. um y-Achse Rot. um
z-Achse
1 0 0 0 X
0 X 0 X
X 0 0
0 X X 0 0
1 0 0 X
X 0 0
0 X X 0 X
0 X 0 0
0 1 0
0 0 0 1 0
0 0 1 0
0 0 1
Im Skript sieht das bis hierhin so aus:
--======= M A T
R I X F U N C T I O N S =======
on GetIdentityMat
return [[1,0,0,0], [0,1,0,0], [0,0,1,0], [0,0,0,1]]
end
on RotateBy aMatrix,
whichAxis, howMuch
-- whichAxis ist Rotationsachse (symbol)
-- howMuch is der Teilwinkel für den aktuellen
Zeitpunkt in Radians
rotationMat = GetIdentityMat()
theCos = cos(howMuch)
theSin = sin(howMuch)
case (whichAxis) of
#x:
rotationMat[2][2]
= theCos
rotationMat[2][3]
= theSin
rotationMat[3][2]
= -theSin
rotationMat[3][3]
= theCos
#y:
rotationMat[1][1]
= theCos
rotationMat[1][3]
= -theSin
rotationMat[3][1]
= theSin
rotationMat[3][3]
= theCos
#z: -- nicht genutzt
rotationMat[1][1]
= theCos
rotationMat[2][2]
= theSin
rotationMat[2][1]
= -theSin
rotationMat[2][2]
= theCos
end case
return Concat(aMatrix, rotationMat)
end
|


 |
|
|
Die Funktion RotateBy()
erstellt zunächst eine Rotations-Matrix, indem sie die Identity-Matrix
je nach Rotationsachse an den oben genannten Stellen modifiziert.
Dafür sind Cosinus- und Sinus-Berechnungen des übergebenen
Winkelwertes nötig. Zurückgegeben wird nicht die Rotationsmatrix,
sondern das mit der Funktion Concat()
erstellte Produkt der als Parameter übergebenen Ausgangsmatrix
aMatrix
und der Rotationsmatrix rotationMat.
In der Funktion TransformMatrix()
wird RotateBy()
zweimal einmal für die X-Rotation, inmal für
die Y-Rotation aufgerufen. Aus der usprünglichen Matrix
wird im ersten prepareFrame
durch die zwei Rotationen folgendes:
myMatrix
ursprüngl.:
[[1, 0, 0, 0], [0, 1, 0, 0], [0, 0, 1, 0], [0, 0, 0, 1]]
myMatrix
nach der 1. Transformation:
[[1, 0.0000, 0.0000, 0], [0, 0.8480, -0.5299, 0], [0, 0.5299, 0.8480,
0], [0, 0.0000, 0.0000, 1]]
myMatrix
nach der 2. Transformation:
[[-0.9613, 0.0000, -0.2756, 0.0000], [-0.1461, 0.8480, 0.5094, 0.0000],
[0.2338, 0.5299, -0.8152, 0.0000], [0.0000, 0.0000, 0.0000, 1.0000]]
Nach der zweiten Transformation enthält myMatrix
die endgültige Transformationsmatrix, die dann in der Funktion
drawQuad()
benutzt wird.
Die Funktion Concat(),
die das Matrizenprodukt erstellt, sieht folgendermaßen aus:
on Concat matA,
matB
if count(matA[1]) <> count(matB) then
alert "bad concat args"
return
end if
product = []
repeat with i = 1 to count(matA)
add product, []
repeat with j = 1 to count(matB)
product[i][j] = 0
repeat with k = 1 to count(matB)
product[i][j] =
product[i][j] + matA[i][k] * matB[k][j]
end repeat
end repeat
end repeat
return product
end
|


 |
|
|
Und wieder in den
zweidimensionalen Raum zurück...
Nach Abarbeitung von TransformMatrix()
enthält myMatrix
die Translationsmatrix, die nun auf die aktuelle Matrix der Quadpunkte
(myQuadOffsets)
angewandt wird. Damit sind wir schon fast wieder bei Bildschirmkoordinaten
angekommen aber nicht ganz. Noch wurden die Quadpunkte im
orthogonalen Raum transformiert; in einem abschließenden Schritt
müssen Sie daher noch auf den zweidimensionalen Bildschirm
projiziert werden.
on DrawQuad me
new3DOffsets = Concat(myQuadOffsets, myMatrix)
screenPos = sprite(spriteNum).loc
newScreenVerts = ScreenTransform(new3DOffsets, screenPos,
¬
myViewDistance)
sprite(spriteNum).quad = newScreenVerts
end
ScreenTransform()
setzt die orthagonalen Werte aus new3Doffsets
in Bildschirmkoordinaten um; dabei kommt die viewDistance
und ein hier hither
genannter Wert zum Tragen, der für die optische Korrektheit
bestimmte Wertebereiche "abschneidet".
on
ScreenTransform pointList, theLoc, viewDistance
hither = 5 -- magic
locList = []
repeat with each in pointList
z = max(each[3] + viewDistance, hither)
newH = (viewDistance * each[1]/z) + (theLoc[1])
newV = (viewDistance * each[2]/z) + (theLoc[2])
add locList, point(newH, newV)
end repeat
return locList
end
Damit ist die Grundfunktionalität
des behaviors abgeschlossen. Die folgende Funktion erlaubt es zusätzlich,
eine Verschiebung der Bitmap im Raum von außen zu starten;
wir werden Sie später im Behavior für die Tastatursteuerung
der Bitmaptransformationen aufrufen.
on
Spinner1_OffsetRotation me, aTriplet, isAbsolute
-- "aTriplet" in der Form [x,y,z], z.B. [-3,
4, 5.2]
-- relative Bewegung, außer wenn isAbsolute TRUE
iat.
if isAbsolute then myQuadOffsets = duplicate(myOrigOffsets)
repeat with each in myQuadOffsets
each[1] = each[1] + aTriplet[1]
each[2] = each[2] + aTriplet[2]
each[3] = each[3] + aTriplet[3]
end repeat
end
|


 |
|
|
Dieses Behavior, das ebenfalls
dem Bitmap-Sprite zugeordnet wird bewegen Sie es vor das
3-D-Spinner-Behavior erlaubt die interaktive Steuerung
einiger Aspekte der 3-D-Animation. Im Beispielfilm auf der CD-ROM
sehen Sie beide Behaviors in Aktion.
-- 3-D-Spinner
Keys
on prepareframe me
if the shiftdown then ¬
set the do_xrot of sprite(me.spritenum)
= 0
else set the do_xrot of sprite(me.spritenum) = 1
if the controldown then ¬
set the do_yrot of sprite(me.spritenum)
= 0
else set the do_yrot of sprite(me.spritenum) = 1
if the commanddown then
if the optiondown then sendSprite(me.spritenum,¬
#Spinner1_OffsetRotation, [1,-1,-5],
0)
else sendSprite(me.spritenum,¬
#Spinner1_OffsetRotation, [-1,1,5],
0)
else sendSprite(me.spritenum,¬
#Spinner1_OffsetRotation, [0,0,0], 1)
pass
end
Mit Shift und Control
können Sie einzelne Apekte der Rotation ausschalten
was praktisch ist, um die Bewegung zu analysieren.
Joachim Gola
|


 |
|