iT邦幫忙

2021 iThome 鐵人賽

DAY 28
0
Modern Web

成為Canvas Ninja ! ~ 理解2D渲染的精髓系列 第 28

Day 28 - 3D繪圖篇 - 2D圖片上面的3D物件是怎麼產生的?II - 成為Canvas Ninja ~ 理解2D渲染的精髓

離賽程結束還有3天~

今天我們要來延續昨天的問題探討~

img

我在上一篇似乎沒有把問題描述的很好,所以可能大家蠻confused的 :(

我這邊再仔細的講講這個問題的細節:

圖片中描述著一個橫置的小箱子,這個小箱子裡面懸吊著兩顆球,兩顆球的水平距離為z。

而現在我們要把這個小箱子的內部景象畫在一張紙上,

這邊雖然是說畫在紙上,不如說是以一點透視的方式『投影』在紙上。

而如果現在紙的位置距離箱子最外邊的一顆球W單位,而這顆球在紙上被投影的長度是原本球的直徑的0.8倍,

那麼請問比較靠裡面的那顆球(與另一顆球間距z),他被投影在紙上時,他的投影長度會是原本球直徑的幾倍?

實際上如果要用上面這張圖來思考幾何比例,可能會不太好理解,所以我們在這邊把這個案例畫成像下面這樣:

img

在這邊我們可以看到投影的射線最後會聚在一個點上,我們會把這個點叫做『視點』。

而因為兩顆球最後都是被投影到同一張紙上,所以我們就可以像上圖這樣透過比例運算去算出來另外一顆球投影在紙上的長度應該會是4W/(5W+z)倍的球直徑長度。

而在這邊,因為4W也就是視點投影面的距離,而5W+z則可以分成4WW+z兩部分,也分別就是(視點投影面)和(投影面物體)的距離。

所以這邊我們其實可以推導出一個關係就是:



透視縮放率(Scale Ratio) = P/(P+Z) 


其中P會是視點投影面的距離(其實就是焦距),Z則是投影面物體的距離,也就是canvas三維座標系下的Z軸座標(假設座標原點就在投影面上)。

有了像這樣的一個公式,我們就有辦法在canvas 上面構築景深關係

我們接下來會用一個Codepen上面的簡單的案例來演示如何渲染景深~

3D景深案例演示

img

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;
  }

在這個方法中,我們可以看到他運用了我們剛剛提到的透視縮放率公式,而且還同時運用在xy軸上,

xy分別是每顆粒子的xy座標,而這個坐標系的中心點是位於canvas正中央。

接著, 有了可以投影x,y座標的方法之後,再來就是只要能夠讓粒子的z軸座標能夠隨時間變化,就能夠產生如同畫面中一般的動畫了~

而這個案例是透過GSAP的TweenMax.to()方法來做循環補間動畫,我們可以在Dot類的constructor(第44行) 看到這個方法的運用方式,

這個方法簡單來說就是可以讓目標物件的property根據時間產生變化,詳細可以看 https://greensock.com/docs/v2/TweenMax/static.to()

在這個案例中我們收穫最大的點就是x,y投影座標的計算,理解了他的計算方式,我們其實就可以試著用canvas畫一個基本的3D物件出來了~

實作: 畫一個立方體

img

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


上一篇
Day 27 - 3D繪圖篇 - 2D圖片上面的3D物件是怎麼產生的? I - 成為Canvas Ninja ~ 理解2D渲染的精髓
下一篇
Day 29 - 3D繪圖篇 - 噪聲地形演算I - 成為Canvas Ninja ~ 理解2D渲染的精髓
系列文
成為Canvas Ninja ! ~ 理解2D渲染的精髓31

1 則留言

0
juck30808
iT邦新手 3 級 ‧ 2021-10-14 12:05:18

恭喜即將邁入完賽啦~

Mizok iT邦新手 5 級 ‧ 2021-10-14 12:48:31 檢舉

謝謝QAQ~

我要留言

立即登入留言