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, 一切靜待後續~