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


<<:  第 28 集:Bootstrap 客制化 component 元件样式

>>:  Day 28:面试

iOS·iOS开发面试-关於底层的那些坑

如果你有时间的,不妨也拿笔本子测试一下,看看能做多少,在文章点赞留言我会第一时间发你答案!或者加我...

# Day 8 Why the “volatile” type class should not be used

今天想要来记录这篇文件 Why the “volatile” type class should n...

Day17 AR装置的编年史(下) 各家公司开始研发各种AR装置

前面说了那麽久,但看起来好像这些都不是拿来给一般民众使用的AR装置,之後AR又有什麽变化呢!?让我们...

Day24 - [丰收款] 以Django Web框架实作永丰API线上支付模拟情境(5) - 我的订单

今天这篇是我们实作库米狗屋●KummyShop的情境电商模拟的最终章了!今天我们要把先前建的一堆订单...

连续 30 天 玩玩看 ProtoPie - Day 3

影片继续看下去 选择 Rotate 底下的 Direction 就是顺时钟还是逆时钟旋转。 我们选择...