Away3d Quaternion 使用四元數來處理空間中的旋轉

| Comments

大家好我是阿邪!

最近因為要計算一些空間中的位移與旋轉

不得不要理解一些數學的運算

要了解3D引擎如何在空間中運算

了解向量與矩陣的基礎知識是必要的

因為大學不是相關科系的

距離上一次算這些東西大概是高中指考前

所以現在關於這些數學觀念全還給高中老師了

標題提到的四元數必須要了解這些概念才會比較好懂!

所以最近幾天都在算數學

向量

其實向量概念很簡單

它可以表示一個座標點、方向跟長度

假設一原點O(0,0)P(a,b)

則OP向量我們可以用P點的座標來表示

空間中的兩點:

空間中有兩點 A(a,b,c)B(d,e,f)

則AB向量是 AB( d-a , e-b , f-c )

這是個簡單的向量

向量長度:

向量A(a,b,c)

長度=Math.sqr(aa+bb+c*c)

向量的運算

向量的加減(位移):

A向量(a,b,c) , B向量(d,e,f)

向量加法
1
加法: A向量+B向量 = (a+d , b+e , c+f);
向量減法
1
減法: A向量 - B向量= (a-d , b-e , c-f);

向量的運算滿足交換律與結合律

向量交換律
1
交換律:A+B=B+A
向量結合律
1
結合律:A+(B+C)=(A+B)+C

向量的乘法(縮放):

向量的乘法
1
常數x乘與向量A(a,b,c)=(x*a , x*b , x*c);

向量的內積(點積):

符號是 兩向量的內積為一個純數 也代表A向量投影在B向量上的分量

向量A(a,b,c)與向量B(d,e,f)

則向量A ‧ B

向量內積
1
A ‧ B=(a*d + b*e + c*f)=A向量的長度*B向量的長度*cosq;

我們可以使用內積來求兩向量的夾角!

向量的外積(叉積):

符號是X

向量的外積還是一個向量

向量A(a,b,c)與向量B(d,e,f)

A X B

向量外積
1
A X B=(a*f-c*e ,  c*d-a*f , a*e-b*d)

得出來的新向量會垂直於A向量與B向量

所以也是A向量與B向量構成的平面上的法向量

另外向量的外積是不滿足交換律的

A X BB X A是不同的!但是兩個得出來的方向剛好相反

矩陣

我們可以將向量轉成矩陣

去運算在空間中的平移與旋轉

空間中的平移會用到矩陣的加法與減法

旋轉會用到矩陣的相乘

所以我們3D引擎裡的rotation X,Y,Z其實背後都是在做旋轉矩陣的運算!

一些算式在這邊都有:點我

關於矩陣的運算與原理

可以看這些:點我

呼!打好久!接下來終於要進入本篇的主題:四元數

四元數不是個數!它是一個數學的模型!

是由愛爾蘭學家威廉·盧雲·哈密頓發現的

有興趣可以去看維基百科:點我

看了以後!發現還是看不懂!只知道他有4個分量w,x,y,z

x,y,z是虛數,w是實數

那跟旋轉有甚麼關係呢?

還好在空間中的四元數不需要從數學的角度去理解(因為我看了好久還是看不懂)

四元數在空間中可以儲存四種分量!畢竟叫四元數就是有四個元素組成

簡單來說可以把四元數表示成:

x,y,z是一個向量!而w是以這個向量為旋轉軸所繞的角度

所以我們可以利用四元數來計算空間中的旋轉

那為什麼要用四元數呢?

以前一般來作空間中的旋轉式使用旋轉矩陣來運算

但它很不好清楚的表示出X,Y,Z的旋轉角度

於是有了歐拉角(Euler)來表示旋轉角度!

但使用歐拉角來表示空間中的旋轉!會有一個很大的問題!

就是萬向死鎖(Gimbal Lock)

簡單來說就是經過3軸旋轉以後~可能會演變成旋轉軸重疊!導至只能轉一個方向

這邊有一個影片可以說明萬向鎖

所以在四元數的實作中可以避免掉萬向鎖的問題

基本上還蠻多地方會用到四元數

比如3D模型的骨架

它們骨塊關節的旋轉都是運用四元數

我們這邊使用四元數來實作一個地球任意方向的旋轉

這邊使用的是away3d 4 beta的3D引擎

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
package
{
import away3d.cameras.Camera3D;
import away3d.containers.Scene3D;
import away3d.containers.View3D;
import away3d.core.math.Quaternion;
import away3d.entities.Mesh;
import away3d.lights.PointLight;
import away3d.materials.lightpickers.StaticLightPicker;
import away3d.materials.TextureMaterial;
import away3d.primitives.SphereGeometry;
import away3d.textures.BitmapTexture;
import away3d.tools.utils.Ray;
import flash.display.Sprite;
import flash.events.Event;
import flash.geom.Matrix3D;
import flash.geom.Vector3D;
/**
 * ...
 * @author sayaku
 */
[SWF(frameRate="60",width="800",height="600")]
public class QuaternionEarth extends Sprite
{
  [Embed(source = "../bin/assets/earth.jpg")]
  private var EarthSkin:Class;
  private var view:View3D;
  private var camera:Camera3D;
  private var scene:Scene3D;
  private var mesh:Mesh;
  private var light:PointLight;
  private var lightPicker:StaticLightPicker;
  private var ray:Ray;//直線運算
  private var SPEED:Number = .97;//減速度
  private var radius:Number = 350;

  public function QuaternionEarth()
  {
      
  if (stage) init(null);
  else addEventListener(Event.ADDED_TO_STAGE, init);
  
  }
      
  private function init(e:Event):void
  {
  removeEventListener(Event.ADDED_TO_STAGE, init);
  initEngine();
  initLight();      
        initObj3D();
  initListener()
  }
      
  private function initEngine():void
  {
  view = new View3D();
  scene = view.scene;
  camera = view.camera;
      
  addChild(view);
  }
      
  private function initLight():void
  {
  light = new PointLight();
  light.y = 5000;
  light.x = -3000;
  light.ambient = .5;
  scene.addChild(light);
  lightPicker = new StaticLightPicker([light]);
  }
      
  private function initObj3D():void
  {
  var sphere:SphereGeometry = new SphereGeometry(radius,50,50);
  var mt:TextureMaterial = new TextureMaterial(new BitmapTexture(new EarthSkin().bitmapData));
  mt.specular = .5;
        mt.gloss = 20;
  mt.ambientColor = 0x333333;
  mt.ambient = 5;
  mt.lightPicker = lightPicker;
  mesh = new Mesh();
  mesh.geometry = sphere;
  mesh.material = mt;
  scene.addChild(mesh);
  ray = new Ray();

  }
      
  private function initListener():void
  {
  addEventListener(Event.ENTER_FRAME, onRender);
  }
      
  private function onRender(e:Event):void
  {
  view.render();
  updataRotation();
      
  }
      
  private function updataRotation():void
  {
 var mouse3DPos:Vector3D = view.unproject(view.mouseX, view.mouseY);
    //利用反投影得到2D平面的滑鼠位置轉換成3D空間中的點
 var currentEarthMt:Matrix3D = mesh.transform;
//取得現在地球在空間中的矩陣
//============================================    
//宣告一個XY平面的四個頂點
   var v1:Vector3D = new Vector3D( -1000, 1000, 0);
   var v2:Vector3D = new Vector3D( 1000, 1000, 0);
   var v3:Vector3D = new Vector3D( 1000, -1000, 0);
   var v4:Vector3D = new Vector3D( -1000, -1000, 0);
//============================================        
   var interSection:Vector3D = new Vector3D;
//相機到空間中滑鼠點的向量與XY平面的交點(也代表原點到此交點的向量)
      
  if (ray.getRayToTriangleIntersection(camera.position, mouse3DPos, v1, v2, v3))
  {
  interSection = ray.getRayToTriangleIntersection(camera.position, mouse3DPos, v1, v2, v3);   
  }
  else
  {        
  interSection = ray.getRayToTriangleIntersection(camera.position, mouse3DPos, v1, v4, v3);   
  }
  interSection ||= new Vector3D;
        //如果是null就NEW一個出來

  interSection.scaleBy(SPEED);
         //向量乘以一個倍數來表示旋轉速度 
  var distance:Number = interSection.length;
        //計算長度       
  var normalDir:Vector3D = new Vector3D(0, 0, 1);
        //XY平面的法向量
  var rotationAxis:Vector3D = interSection.crossProduct(normalDir);
        //兩向量作外積可得到垂直於兩向量的向量(即我們需要的旋轉軸)
  rotationAxis.normalize();
        //旋轉軸記得要正規化!不然轉起來會頓頓
  var q:Quaternion = new Quaternion();
        //四元數
  q.fromAxisAngle(rotationAxis, (distance / radius));
  q.normalize();
        //如果沒有再作其他四元數的運算,不正規化也沒關係
  
  currentEarthMt.append(q.toMatrix3D());
        //矩陣運算(兩矩陣相乘)改變目前地球的旋轉角度
  mesh.transform=currentEarthMt ;
        //再指定給現在的地球
  }
      
   }

}

看一下這個地球旋轉用到的概念

er

從這張圖可以講解這個DEMO的基本概念

基本上我們的目的就是要算出地球的旋轉軸

首先我們利用unproject將2D的滑鼠位置轉成在空間中的位置,轉換後z值是20,我也不知道為什麼!蠻特殊的轉換法

接下來有了滑鼠在空間中的點就能算攝影機到這個點的向量

然後要算出這個向量跟XY平面的交點則是利用Ray類別裡的getRayToTriangleIntersection()方法

這個方法只能算你給的三角形面與這個向量的交點(其實平面就是兩個三角型組成的)

getRayToTriangleIntersection()的參數分別是(“向量的起點”,“向量的終點”,“三角形的三個頂點”)

如果沒有交點的話會回傳null

所以將我們定義的XY平面的頂點拆成兩個三角形去用if來判斷現在是在哪個平面有交點

算出交點後~就表示地球是要往你那個交點方向旋轉

那要算出旋轉軸就很簡單了

只要交點這個向量與Z軸向量作外積就可以做出垂直於這兩個向量的向量也就是我們要的旋轉軸

得到向量的旋轉軸要做正規化的動作

因為我們只需要這個向量的方向~不需要它的長度跟座標!所以將它正規化

正規化的意義就是使這個向量的長度為1,如果你不做正規化!在運算時可能會超出範圍!

所以務必要使用向量正規化!正規化後原本的座標與長度就被蓋掉了!這點要注意

然後再用四元數去處理旋轉就OK了

基本上四元數能算旋轉但是不能直接給出角度!所以還是要轉成矩陣去做矩陣運算

原始碼下載:點我

如果數學概念有錯的話請指正:)

Comments