離賽程結束還有3天~
今天我們要來延續昨天的問題探討~
我在上一篇似乎沒有把問題描述的很好,所以可能大家蠻confused的 :(
我這邊再仔細的講講這個問題的細節:
圖片中描述著一個橫置的小箱子,這個小箱子裡面懸吊著兩顆球,兩顆球的水平距離為z。
而現在我們要把這個小箱子的內部景象畫在一張紙上,
這邊雖然是說畫在紙上,不如說是以一點透視的方式『投影』在紙上。
而如果現在紙的位置距離箱子最外邊的一顆球W單位,而這顆球在紙上被投影的長度是原本球的直徑的0.8倍,
那麼請問比較靠裡面的那顆球(與另一顆球間距z),他被投影在紙上時,他的投影長度會是原本球直徑的幾倍?
實際上如果要用上面這張圖來思考幾何比例,可能會不太好理解,所以我們在這邊把這個案例畫成像下面這樣:
在這邊我們可以看到投影的射線最後會聚在一個點上,我們會把這個點叫做『視點
』。
而因為兩顆球最後都是被投影到同一張紙上,所以我們就可以像上圖
這樣透過比例運算去算出來另外一顆球投影在紙上的長度應該會是4W/(5W+z)
倍的球直徑長度。
而在這邊,因為4W
也就是視點
到投影面
的距離,而5W+z
則可以分成4W
和W+z
兩部分,也分別就是(視點
到投影面
)和(投影面
到物體
)的距離。
所以這邊我們其實可以推導出一個關係
就是:
透視縮放率(Scale Ratio) = P/(P+Z)
其中P
會是視點
到投影面
的距離(其實就是焦距
),Z
則是投影面
到物體
的距離,也就是canvas
三維座標系下的Z軸
座標(假設座標原點就在投影面上)。
有了像這樣的一個公式,我們就有辦法在canvas
上面構築景深
關係
我們接下來會用一個Codepen
上面的簡單的案例來演示如何渲染景深~
Codepen: https://codepen.io/team/basedesign/pen/mvJQWX
備用連結: https://codepen.io/mizok_contest/pen/GRvJGNG
其實這是一個我在逛
codepen
時偶然發現的一個小型實作,而這個案例還蠻適合用來講解這次提及的主題。
我們可以看到這個作者在畫面中創造了大量的方形粒子,然後讓他們在3D
空間中移動。
然而這是怎麼做到的呢?
首先我們可以先看看核心的透視計算部分
,也就是在54
行的project
方法(隸屬於Dot
類)
// Do some math to project the 3D position into the 2D canvas
project() {
this.scaleProjected = PERSPECTIVE / (PERSPECTIVE + this.z);
this.xProjected = (this.x * this.scaleProjected) + PROJECTION_CENTER_X;
this.yProjected = (this.y * this.scaleProjected) + PROJECTION_CENTER_Y;
}
在這個方法中,我們可以看到他運用了我們剛剛提到的透視縮放率
公式,而且還同時運用在x
和y
軸上,
x
和y
分別是每顆粒子的x
和y
座標,而這個坐標系的中心點是位於canvas
正中央。
接著, 有了可以投影x
,y
座標的方法之後,再來就是只要能夠讓粒子的z
軸座標能夠隨時間變化,就能夠產生如同畫面中一般的動畫了~
而這個案例是透過GSAP的TweenMax.to()方法來做循環補間動畫
,我們可以在Dot
類的constructor
(第44行) 看到這個方法的運用方式,
這個方法簡單來說就是可以讓目標物件的property根據時間產生變化,詳細可以看 https://greensock.com/docs/v2/TweenMax/static.to()
在這個案例中我們收穫最大的點就是x
,y
投影座標的計算,理解了他的計算方式,我們其實就可以試著用canvas
畫一個基本的3D
物件出來了~
Codepen: https://codepen.io/mizok_contest/pen/vYJOaZq
其實講完了上面的案例之後,這個案例應該就顯得很簡單了~
但是我還是講講程式的細節~
function draw(ctx,dots,size){
const projective = 500; //假定透視焦距為500
dots.forEach((o,i)=>{
// 計算透視投影後的座標陣列
const projectArr = project(o[0],o[1],o[2], projective,ctx.canvas);
const px= projectArr[0];
const py= projectArr[1];
// 把座標點位畫出來
drawCircle(ctx, px, py, 5, 'white', 1);
// 如果座標跟座標之間的距離剛好是邊長,那就連線
for(let j =0;j<dots.length;j++){
if(dist(dots[i],dots[j])==size){
let projectArrAnother = project(dots[j][0],dots[j][1],dots[j][2], projective,ctx.canvas);
ctx.beginPath();
ctx.moveTo(px,py);
ctx.lineTo(projectArrAnother[0],projectArrAnother[1]);
ctx.lineWidth=5;
ctx.strokeStyle="white";
ctx.stroke();
ctx.closePath();
}
}
})
}
到這邊為止我們就成功畫出了第一個透視3D物件了,而且我們還可以透過操作x/y/z值讓他在空間中移動! 在接下來的系列文中,我們會提到稍微進階一點的操作,敬請期待~ :D