Flash與3D編程探祕(六)- 全方位旋轉攝像機

前面討論過了如何橫向旋轉和移動攝像機,希望你已經完全理解,因爲本文中的內容緊接着上一篇。回想一下,在前面製做的動畫中,攝像機的旋轉一直是圍繞着y軸(豎直向上的軸)旋轉,然而現實中我們可以上下旋轉攝像機,甚至可以把攝像機傾斜一定角度,這就提醒了大家還需要更深入的研究旋轉這個課題。下面幾個動畫演示了攝像機(簡單的攝像機輪廓)3種旋轉模式,從左到右分別是橫向旋轉,縱向旋轉,傾斜。從3D空間角度來說,分別是沿y,x和z軸旋轉。

 

      

橫向和縱向旋轉攝像機



傾斜攝像機


再介紹一些三角函數方程

事情變得複雜起來了!不過請你還是保持頭腦清醒,這一篇文章你的任務就是學會縱向旋轉和傾斜攝像機。你也許會想,我已經學會了橫向旋轉攝像機,算法中使用了一個panning的變量代表旋轉角度,那麼我再加兩個變量,然後按順序先沿x旋轉,然後再沿y,最後z旋轉不就好了。Well,這種想法很接近但卻是錯的(不要把3D數學想的那麼簡單)。來看一下下面的兩個圖,還是以3D空間在2D平面上的投影舉例,把線段OB沿着z軸(也就是2D平面上 的原點)旋轉大約78度,可以看到OB的長度就是我們的旋轉半徑。那麼接下來,把OB沿y軸旋轉,看一下圖中的旋轉半徑是多少?不難看出,旋轉半徑明顯的變小了。繼續,如果你沿y旋轉後,再打算沿x軸旋轉,你還是會有同樣“半徑變化”的問題。

先沿z軸旋轉再試圖沿y旋轉

 

在上面的例子中,如果想要保持旋轉半徑不變,那麼一開始就不要有旋轉角度。不過拿着攝像機左轉右轉,怎麼可能保持角度不變!?因此你要在每一次攝像機旋轉後,計算新的旋轉半徑。可是如果每一次都把旋轉半徑計算出來,那一定是很頭疼!


於是人們聰明的想到了省力的方法。先來再看一下2D的三角函數(又是三角函數),根據旋轉半徑和旋轉角度可以得到x和y:
<!--

Code highlighting produced by Actipro CodeHighlighter (freeware)
http://www.CodeHighlighter.com/

-->= Math.cos(angle)*radius;
= Math.sin(angle)*radius;


需要注意一點,上面的方程式旋轉點爲原點,並且之前旋轉角度爲0。如果之前就有旋轉角度a,再旋轉b,那麼方程式就成了:

<!--

Code highlighting produced by Actipro CodeHighlighter (freeware)
http://www.CodeHighlighter.com/

-->= Math.cos(a+b)*radius;
= Math.sin(a+b)*radius;

看起來眼熟,但是不知道是什麼了?別擔心:
<!--

Code highlighting produced by Actipro CodeHighlighter (freeware)
http://www.CodeHighlighter.com/

-->cos(a+b) = cos(a)*cos(b) - sin(a)*sin(b);
sin(a
+b) = sin(a)*cos(b) + sin(b)*cos(a);

把cos(a+b)和sin(a+b)帶入上面的x和y求值方程我們就有:
<!--

Code highlighting produced by Actipro CodeHighlighter (freeware)
http://www.CodeHighlighter.com/

-->= radius*cos(a)*cos(b) - radius*sin(a)*sin(b);
= radius*sin(a)*cos(b) + radius*sin(b)*cos(a);

化簡一下得到:
<!--

Code highlighting produced by Actipro CodeHighlighter (freeware)
http://www.CodeHighlighter.com/

-->= x_before*cos(b) - y_before*sin(b);
= x_before*sin(b) + y_before*cos(b);

這樣,使用上面兩個方程,不用擔心你在其他平面(xz或者yz平面)的旋轉角度,也不用每一次旋轉後再去計算物體新的旋轉半徑了,只要關心旋轉後的x和y,並且把它們作爲下一次旋轉的x_before和y_before,問題就解決了。下面我把相應的方程式寫上:

圍繞y軸旋轉pan角度:
<!--

Code highlighting produced by Actipro CodeHighlighter (freeware)
http://www.CodeHighlighter.com/

-->= Math.cos(pan)*x_before - Math.sin(pan)*z_before;
= Math.sin(pan)*x_before + Math.cos(pan)*z_before;

圍繞x軸旋轉pitch角度:
<!--

Code highlighting produced by Actipro CodeHighlighter (freeware)
http://www.CodeHighlighter.com/

-->= Math.cos(pitch)*y_before - Math.sin(pitch)*z_before;
= Math.sin(pitch)*y_before + Math.cos(pitch)*z_before;

圍繞z軸旋轉tilt角度:
<!--

Code highlighting produced by Actipro CodeHighlighter (freeware)
http://www.CodeHighlighter.com/

-->= Math.cos(tilt)*x_before - Math.sin(tilt)*y_before;
= Math.sin(tilt)*x_before + Math.cos(tilt)*y_before;


那麼基本的知識已經說完了。總結一下,當你在使用這些方程式操作攝像機全方位旋轉的時候,只要取得相應的變量,然後替換在方程裏就可以了。是不是看起來有點難理解?不要擔心,適應這些東西是需要花一點時間(特別是這些對你來說還是新課題的話),不過適應以後你應該就覺得很簡單了。坦白的說,其實你並不需要知道到底這些是怎樣得來的,你只要知道如何使用它們,得到想要得結果就可以了(當然完全理解會對你以後的學習有一些幫助)。這些方程你可以寫成一個函數,然後命名它爲“給我旋轉”方程,當你需要攝像機旋轉的時候,只要呼喚“給我旋轉”就好了,至於“給我旋轉”怎麼做的工作,你就不需要擔心了。

 

 

全方位旋轉攝像機 

只說這些理論的東西,你肯定會覺得乏味,那麼我舉個例子來說明。下面這個程序演示了攝像機的全方位旋轉,運行程序你會看到你置身在一個巨大的正方體中,這個正方體是由很多我們的朋友小P組成的,不過這回用不同顏色的小P來代表不同的邊。使用WS鍵控制縱向旋轉,AD鍵控制橫向旋轉,QE控制傾斜角度,鼠標點擊屏幕禁止或者允許鼠標移動控制縱向和橫向旋轉。



全方位旋轉攝像機,使用WS縱向旋轉,AD橫向旋轉,QE傾斜角度,鼠標點擊禁止或者允許鼠標控制


製作步驟:

1. 首先定義幾個常量,MAX_OBJ是每條邊上的物體數量,CUBE_WIDTH是正方體的邊長。

<!--

Code highlighting produced by Actipro CodeHighlighter (freeware)
http://www.CodeHighlighter.com/

-->// constants
var MAX_OBJ = 6;
var PI 
= 3.1415926535897932384626433832795;
var CUBE_WIDTH 
= 300;

 

 

2. 下面還是設置原點,場景,焦距等。

<!--

Code highlighting produced by Actipro CodeHighlighter (freeware)
http://www.CodeHighlighter.com/

-->// same as usual
var origin = new Object();
origin.x 
= stage.stageWidth/2;
origin.y 
= stage.stageHeight/2;
origin.z 
= 0;

var scene 
= new Sprite();
scene.x 
= origin.x;
scene.y 
= origin.y;
this.addChild(scene);

var focal_length 
= 400;

 

 

3. 設置攝像機,這回的攝像機多了幾個新的屬性,pitching是縱向旋轉角度,tilt是傾斜的角度。

<!--

Code highlighting produced by Actipro CodeHighlighter (freeware)
http://www.CodeHighlighter.com/

-->var camera = new Object();
camera.x 
= 0;
camera.y 
= 0;
camera.z 
= 0;
camera.panning 
= -PI/8;                // init pan angle of our camera, pan left
camera.pitching = -PI/8;               // pitch up
camera.tilt = 0;                           // and no tilt

 

 

4. 設置一些全局變量,在處理鍵盤和鼠標事件時會用到。

<!--

Code highlighting produced by Actipro CodeHighlighter (freeware)
http://www.CodeHighlighter.com/

-->// global booleans for our keyboard control
var pan_left;
var pan_right;
var pitch_up;
var pitch_down;
var mouse_ctl 
= true;

 

 

5. 下面佈置一個正方體,你完全不必要明白我是怎麼佈置場景的,因爲佈置場景的方式並不唯一,所以如果你願意的話,你可以自己動手佈置3D場景,當然我也不介意直接拷貝我的設置場景的代碼去用。總之,這些代碼就是初始化一些小P,然後把它們擺放在合適位置(圍繞着攝像機)。

<!--

Code highlighting produced by Actipro CodeHighlighter (freeware)
http://www.CodeHighlighter.com/

-->// ok, here you don't have to know how i acutally setup the cube
// cause every body has a different way of doing that, if you really
// interested in how i did it, then you may have a look
// you can just copy my code and it will set it up for you
var len = CUBE_WIDTH/2;
for (var seg = 0; seg < 3; seg++)
{
    var line_h;
    var line_v;    
    var line_z;
    
    
switch (seg)
    {
        
case 0:
            line_h 
= true;
            line_v 
= false;
            line_z 
= false;
            
break;
        
case 1:
            line_h 
= false;
            line_v 
= true;
            line_z 
= false;
            
break;
        
case 2:
            line_h 
= false;
            line_v 
= false;
            line_z 
= true;
            
break;
    }
    
    
for (var i = 0; i < MAX_OBJ; i++)
    {
        
if (line_h || (i == 0 || i == MAX_OBJ-1))
        {
            
for (var j = 0; j < MAX_OBJ; j++)
            {
                
if (line_v || (j == 0 || j == MAX_OBJ-1))
                {
                    
for (var k = 0; k < MAX_OBJ; k++)
                    {
                        
if (line_z || (k == 0 || k == MAX_OBJ-1))
                        {
                            var ball;
                            
if (line_h)
                            {
                                ball 
= new SphereHorizontal();
                                
if ((i == 0 || i == MAX_OBJ-1))
                                {
                                    ball 
= new SphereVertex();
                                }
                                
else
                                {
                                    ball 
= new SphereHorizontal();
                                }
                            }
                            
if (line_v)
                            {
                                ball 
= new SphereVertical();
                                
if ((j == 0 || j == MAX_OBJ-1))
                                {
                                    
continue;
                                }
                            }
                            
if (line_z)
                            {
                                ball 
= new SphereStraight();
                                
if ((k == 0 || k == MAX_OBJ-1))
                                {
                                    
continue;
                                }
                            }
                            ball.x_3d 
= -len + (i)*(CUBE_WIDTH/MAX_OBJ);
                            ball.y_3d 
= -len + (j)*(CUBE_WIDTH/MAX_OBJ);
                            ball.z_3d 
= -len + (k)*(CUBE_WIDTH/MAX_OBJ);
                            scene.addChild(ball);
                        }
                    }
                }
            }
        }
    }
}

 

 

6. 下面的函數就是刷新小P位置和大小的函數,這也是這篇文章主要講述的內容,所以,請集中。OK,首先要得出小P(其中一個小P)到攝像機的x,y和z 距離。對於橫向旋轉角度panning,使用本文前面講述的方程,把相應的x距離和z距離帶入,然後得出新的x距離和z距離。使用相同的方法得出 縱向旋轉角度pitching後的y和z距離,對攝像機傾斜角度tilt再次使用上述方程。這樣就得到圍繞三個軸旋轉後新的x,y和z距離,繼而便可以使用老辦法算出物體的縮放和移動。最後別忘記加一個z_near變量,存儲小P到攝像機的距離,以便於對所有小P進行z排序。

<!--

Code highlighting produced by Actipro CodeHighlighter (freeware)
http://www.CodeHighlighter.com/

-->// update ball size and position
// here is what we really care about, so concentrate
function display(obj)
{    
    var x_distance 
= obj.x_3d - camera.x;         // first we determine x distance ball to camera
    var y_distance = obj.y_3d - camera.y;        // y
    var z_distance = obj.z_3d - camera.z;        // z distance
    
    var tempx, tempy, tempz;                       
// some temporary variables
    
    
// two more trig you need to know about, suppose a is the previous angle
    
// cos(a+b) = cos(a)*cos(b) - sin(a)*sin(b)
    
// sin(a+b) = sin(a)*cos(b) + cos(a)*sin(b)
    
// thus we have the following
    var angle = camera.panning;
    tempx 
= Math.cos(angle)*x_distance - Math.sin(angle)*z_distance;
    tempz 
= Math.sin(angle)*x_distance + Math.cos(angle)*z_distance;
    x_distance 
= tempx;
    z_distance 
= tempz;
    
    angle 
= camera.pitching;                    // the same thing we have for pitch angle
    tempy = Math.cos(angle)*y_distance - Math.sin(angle)*z_distance;
    tempz 
= Math.sin(angle)*y_distance + Math.cos(angle)*z_distance;
    y_distance 
= tempy;
    z_distance 
= tempz;
    
    angle 
= camera.tilt;                          // and tilt angle
    tempx = Math.cos(angle)*x_distance - Math.sin(angle)*y_distance;
    tempy 
= Math.sin(angle)*x_distance + Math.cos(angle)*y_distance;
    x_distance 
= tempx;
    y_distance 
= tempy;
    
    
if (z_distance > 0)                                           // if the ball isin front of the camera
    {
        
if (!obj.visible)                                
            obj.visible 
= true;                                    // make the ball visible anyway
            
        var scale 
= focal_length/(focal_length+z_distance);   // cal the scale of the ball
        obj.x = x_distance*scale;                             // calcualte the x position in a camera view 
        obj.y = y_distance*scale;                            // and y position
        obj.scaleX = obj.scaleY = scale;                    // scale the ball to a proper state
    }
    
else
    {
        obj.visible 
= false;
    }
    
    obj.z_near 
= z_distance;            // keep track of z distance to our camera
}

 

 

7. 寫一個循環函數,不停的調用第6步的函數刷新所有的小P。如果你一直有看文章的話那麼這些對你來說應該不難。

<!--

Code highlighting produced by Actipro CodeHighlighter (freeware)
http://www.CodeHighlighter.com/

-->// loop to update the screen
function run(e:Event)
{
    
for (var i = 0; i < scene.numChildren; i++)                  // update all the balls on the screen
    {
        display(scene.getChildAt(i));
    }
    
    swap_depth(scene);
}

// bubble sort algo
function swap_depth(container:Sprite)
{
    
for (var i = 0; i < container.numChildren - 1; i++)
    {
        
for (var j = container.numChildren - 1; j > 0; j--)
        {
            
if (Object(container.getChildAt(j-1)).z_near < Object(container.getChildAt(j)).z_near)
            {
                container.swapChildren(container.getChildAt(j
-1), container.getChildAt(j));
            }
        }
    }
}

 

8. 最後是設置一些鍵盤和鼠標事件響應函數,完成我們的程序。

<!--

Code highlighting produced by Actipro CodeHighlighter (freeware)
http://www.CodeHighlighter.com/

-->function key_down(e:KeyboardEvent):void
{
    
if (e.keyCode == 65)            // a
        pan_left = true;
    
if (e.keyCode == 68)            // d
        pan_right = true;
    
if (e.keyCode == 87)            // w
        pitch_up = true;
    
if (e.keyCode == 83)            // s
        pitch_down = true;
}
function key_up(e:KeyboardEvent):
void
{
    
    
if (e.keyCode == 65)
        pan_left 
= false;
    
if (e.keyCode == 68)
        pan_right 
= false;
    
if (e.keyCode == 87)
        pitch_up 
= false;
    
if (e.keyCode == 83)
        pitch_down 
= false;
}
function key_response(e:Event):
void
{
    
if (pan_left)
        camera.panning 
-= 0.01;
    
if (pan_right)
        camera.panning 
+= 0.01;
    
if (pitch_up)
        camera.pitching 
-= 0.01;
    
if (pitch_down)
        camera.pitching 
+= 0.01;
        
    
if (mouse_ctl)                                // if allow mouse control pan and pitch
    {
        camera.panning 
+= scene.mouseX/22000;
        camera.pitching 
+= scene.mouseY/22000;
    }
    
    
// limit the pitch and tilt
    if (camera.pitching < -1*PI/3)
        camera.pitching 
= -1*PI/3;
    
if (camera.pitching > PI/3)
        camera.pitching 
= PI/3;
}
function clicked(e:Event)                        
// toggle mouse control
{
    mouse_ctl 
= !mouse_ctl;
}

// setup event listeners
this.addEventListener(Event.ENTER_FRAME, run);
this.addEventListener(Event.ENTER_FRAME, key_response);
stage.addEventListener(KeyboardEvent.KEY_DOWN, key_down);
stage.addEventListener(KeyboardEvent.KEY_UP, key_up);
stage.addEventListener(MouseEvent.CLICK, clicked);

 

 

總結一下,這篇文章中的三角函數部分可能有些抽象,因爲是在3D空間中完成的,如果你把所有的例子首先映射到2D平面上會想對好理解一些。不過還是那句話,不要擔心,你只要知道如何使用這些方程就可以了。

 

注意:物體自身圍繞中心的3D旋轉

有一點不知道你有沒有注意,那就是在本文的開頭,我做了幾個攝像機的旋轉演示。在演示裏,攝像機(物體)都是本身在旋轉,而並不是我們的眼睛(攝像機)在旋轉。雖然到目前爲止的文章裏,還沒有討論到如何讓物體自身旋轉,不過很快就會看到一些例子。注:我做這些演示唯一想說明的就是三種旋轉的機制,所以請不要着急那些演示是怎麼做出來的。

 

其實,關於Flash和3D空間的基本知識的介紹到這裏我想應該結束了,從下篇文章開始就要關注3D物體,因此,如果對前面文章中的基礎知識還是模模糊糊的話,完全可以不必擔心。不過我還是建議你自己多實驗一些小例子,增加自己的空間感。相信你在不久的將來就可以開發自己的3D Engine了,加油,ALL THINGS ARE POSSIBLE!


上一篇          目錄          下一篇

非常抱歉,文中暫時不提供源文件下載,如果你需要源文件,請來信或者留言給我。筆者利用工作之餘寫這些文章,付出了很多汗水,希望讀者和轉載者能夠尊重作者的勞動。

作者:Yang Zhou
出處:http://yangzhou1030.cnblogs.com
本文版權歸作者和博客園共有,轉載未經作者同意必須保留此段聲明。請在文章頁面明顯位置給出原文連接,作者保留追究法律責任的權利。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章