Raycast實現僞3d遊戲(HTML5) (一)

http://dev.opera.com/articles/view/creating-pseudo-3d-games-with-html-5-can-1/ html

參考此文~ canvas


純粹的3d世界的夠在如今已經很是的成熟了, 就是各類亂七八糟的技術很是之多,不免讓人摸不着頭腦。 函數

那ok, 能夠回到遠古時代,追尋一些簡單的方法,去實現一些simple的東西,畢竟這個世界的構造老是從simple到複雜的~ 測試

固然本身手動的將3d的世界展示在2d的平面上,多少有些軟件繪製的味道,那麼先從理論講起。 3d

raycast的意思,據我理解就是從人眼中發出光線,經過一個屏幕,最後和一個世界中的物體相交,探查到物體表面的一些性質,一般是顏色~ htm

從這種最樸素的觀點來看(古希臘人就有相似的關於光的概念),也很好的控制了世界的範圍,所謂你的世界,就只是你所感知的世界。 遊戲

可是若是隻是上圖的話還有些具體的問題,眼睛在哪裏?屏幕多大?屏幕在哪裏?視線的範圍又有多大?人眼的視線射向無窮遠處又該怎麼辦? it

ok,從人頭上方俯視能夠假設這個水平的可視範圍是90度的角, 屏幕的大小能夠假設是320*200, 人眼正對屏幕中心。 io

ok,肯定了人眼的範圍,還須要肯定屏幕到人眼的距離, 屏幕高度200, 寬度320, 你的眼睛角度是90, 那麼一半是45度,這個距離應該不難吧,是160。 ast

肯定了人和屏幕,接着就要肯定世界是什麼樣子,以及人應該在世界的什麼地方。

一個普通的2d迷宮版的世界,周圍都是牆,應該是一個比較簡單的世界(室內世界)。有着天花板和地面, 以及一個四四方方的牆。


ok, 這個世界能夠是1000*1000的大小,從頭上看被切分紅了10*10的小塊,每塊100*100大小(不要問我上面的圖不對~我gimp不好的)

ok,如今有了世界,有了人,有了屏幕,那麼接下來就是從眼睛裏面發出光線了

一般遊戲都只是一個循環。

function gameCycle()

{

     keyInput();

     draw();

     setTimeout(gameCycle, 1000/30);

}

遊戲的實體包括玩家:

var player = {

     x:5,

     y:5,

    dir:0,

};

地圖

var map = [

[1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1],

[1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1],

[1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1],

[1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1],

[1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1],

[1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1],

[1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1],

[1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1],

[1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1],

];

這些1表示這個位置是牆,0表示沒有東西, 下面須要探索這個世界了, 首先須要肯定世界的座標系是什麼, 即原點,x軸,y軸,旋轉的正方向~(額,就是逆時針仍是順時針旋轉,和某個軸的夾角)

水平向右是x軸正方向,豎直向下是y軸正方向, 順時針旋轉是正方向,夾角是和x軸的夾角。

ok,有了這些瑣碎麻煩的前提條件以後, player的信息也明確了。

玩家是在5*100, 5*100, 的地圖位置, 面朝的是右側。

可是上面只是明確了世界座標, 咱們還要明確屏幕座標, 就是你如今看的這個屏幕的座標系統~

原點也是在右上角, x軸水平向右, y軸正方向 水平向下~ 寬度320 高度 200~ 你如今距離屏幕160  牆高200, 中心在100的高度位置~


如何把玩家看到的東西畫到屏幕上呢,咱們須要從玩家到屏幕發射一條條的光線~

能夠屏幕上每一個像素和玩家的眼睛之間連線,畫出一條光線, 可是這個數量有點多,能夠認爲全部牆都是垂直於地面的,那麼,能夠只繪製水平的幾百條光線,而牆的在屏幕上的高度由距離來決定。


好吧如今,把你的世界和你的屏幕結合起來一塊兒看看

那麼咱們的draw函數就是

function draw()

{

   //咱們從屏幕的左到右發出光線

    for(var i  = 0; i < 320; i++)//320 條光線

    {

         var dir = [100,  -(i-100)];//光線在世界中的方向 世界中的 x和y方向~ 不要和屏幕 中的方向混淆了

         //那麼沿着這個方向, 考慮玩家的位置 開始發送光線吧

        castRay(player.x, player.y,  dir[0], dir[1]);

    }

}

function castRay(oriX, oriY, dirX, dirY)

{    

}


有個問題,怎麼發送光線來判斷和哪堵牆相交, 好吧怎麼判斷一條射線和一個矩形相交 ??

首先這個世界被一個個的小方塊分割, 咱們能夠從玩家所在的點 , 沿着光線的方向和一個個的小方塊作相交測試。

好吧,第一步怎麼一點點增長光線的長度, 第二步, 相交在小方塊的哪條邊上,邊上的哪一個點上???

聽說最好的方法是 把x 和y軸分開分別進行測試, 由於x, y自己是平行線 而且和座標軸平行, 根據平行線的理論, 被等距離平行線分割的線段的長度都是相等的,不信你看~

好吧我認可這個不太平行~ 至於爲何, 這個根據歐基裏德的幾何學,平行線之間的距離是他們的垂線段的長度, 這個長度相等, 而一條直線和平行線的夾角都是相等的(爲何?),那麼簡單的角角邊, 或者直接直角三角形求變長均可以證實 斜線段相等。

固然特例是當斜線段自己垂直於平行線的時候, 須要額外考慮~

額,好吧,再追本溯源列出歐基裏德的公理:

  公設1:任意一點到另外任意一點能夠畫直線。  

        公設2:一條有限線段能夠繼續延長。  

       公設3:以任意點爲心及任意的距離能夠畫圓。  

       公設4:凡直角都彼此相等。  

       公設5:同平面內一條直線和另外兩條直線相交,若在某一側的兩個內角和小於二直角的和,則這二直線經無限延長後在這一側相交。

      好吧由於公理5, 因此夾角是相等的~~ 終於搞清了一個這個初等幾何題

       固然還有一些顯而易見的幾何計算方法~

       1、等量間彼此相等 ;  2、等量加等量和相等 ;  3、等量減等量差相等 ;  4、徹底重合的東西是相等的 ;  5、總體大於部分


好吧,如今咱們成功的證實了平行線切分線段的長度相等, 那麼咱們發出的光線在前進的過程當中 被x軸,y軸這些軸線切分的線段也相等的

那麼處理過程能夠分爲兩部分,

首先是沿x軸的平行線進行切分, 先求最近的和格子交點, 再每次移動一格的寬度,加上相應的高度,獲得對應的格子編號,判斷格子是否有牆,記錄下當前和牆交點和玩家的距離


首先如何計算一條射線和一條一個方框的交點呢?

首先搞清射線在平面上如何表示,採用參數表示方法就是

(x, y) = (x0, y0) + k*(dx, dy) k>=0

其中x0, y0 是射線的出發點, dx, dy 是射線的方向, k 是參數

那麼,若是是和豎直方向的軸相交, 那麼x值就是 Math.ceil(player.x) * 100 ,(注意玩家當前是面向右邊的)

相應的k就能夠獲得:k = (x-x0)/dx

那麼相應的y值就是: y = y0+k*dy


ok,獲得了初始點, 再計算每次沿x軸移動100, 對應的y移動的距離是:

DX = 100

DY = 100*dy/dx


ok, 在判斷當前移動到的格子是否有牆存在:

Math.floor(x/100),  Math.floor(y/100)


代碼以下:

function castRay(oriX, oriY, dirX, dirY)

{

      var x = oriX*100;

      var y = oriY*100;

      var k = dirY/dirX;

      var tx = Math.ceil(x);

      var ty = k*(tx-x)+y;

      //初次交點就是tx, ty 點

      var DX = 100;

      var DY = 100*dirY/dirX;

      var dist = 0;

      while(true)

     {

            var wallX = Math.floor(tx/100);

            var wallY = Math.floor(ty/100);

           if(map[wallX][wallY] != 0)

          {

                    break;

            }

            tx += DX;

            ty += DY;

      }

     

}

判斷有牆了以後, 還須要計算玩家到牆交點的距離是多少, 這個簡單, 根據歐基裏德的距離公式:

dist = Math.sqrt( (tx-x)*(tx-x) + (ty-y)*(ty-y) )


知道了距離, 接着就是要把牆投影到屏幕上了, 假設牆高度都是200, 那麼投影到屏幕上的高度就是:

height = 200* viewDist/dist

這裏的viewDist是人到屏幕的距離, dist 是人到牆的距離, height是牆投影到屏幕上的高度~

所以咱們須要添加一個繪製相應高度線段的函數, drawSeg(height, idx)

參數是線段的高度和線段在屏幕上的x座標,


那麼修改後的castRay函數就是:

function castRay(oriX, oriY, dirX, dirY, idx)

{

      var x = oriX*100;

      var y = oriY*100;

      var k = dirY/dirX;

      var tx = Math.ceil(x);

      var ty = k*(tx-x)+y;

      //初次交點就是tx, ty 點

      var DX = 100;

      var DY = 100*dirY/dirX;

      var dist = 0;

      while(true)

     {

            var wallX = Math.floor(tx/100);

            var wallY = Math.floor(ty/100);

           if(map[wallX][wallY] != 0)

          {

                    break;

            }

            tx += DX;

            ty += DY;

      }

      var height = 200* viewDist/dist

      drawSeg(height,  idx);

}


總體的結構就是:

draw()

{

    for(var i  = 0; i < 320; i++)//320 條光線

    {

         var dir = [100,  -(i-100)];//光線在世界中的方向 世界中的 x和y方向~ 不要和屏幕 中的方向混淆了

         //那麼沿着這個方向, 考慮玩家的位置 開始發送光線吧

        castRay(player.x, player.y,  dir[0], dir[1], i);

    }

}


至於drawSeg 則和具體的canvas如何在屏幕上畫一條線段相關~



後續:

這裏咱們只考慮了一個很是特殊的 狀況, 一個靜止的面向右向的玩家所看到的世界。

咱們只考慮了和豎直軸相交的問題, 還要考慮和水平軸相交的問題, 處理方法相似。

這個世界看起來也很是奇怪, 這種奇特的現象叫作魚眼效應, 所以也要想辦法消除~

ok, 一切靜待後續~

相關文章
相關標籤/搜索