D7WS HOMEPAGE WORKSHOP NEUE XTRAS GALERIE WORKSHOP Automatic Translation LESERSERVICE BUCH NEWS

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





Directorworkshop.de ist © Joachim Gola & Gerd Gillmaier 1998-2002. Alle Rechte vorbehalten.