Canvas之蛋疼的正方體繪制體驗


事情的起因

  之前寫了篇談談文字圖片粒子化 I,並且寫了個簡單的demo -> 粒子化。正當我在為寫 談談文字圖片粒子化II 准備demo時,突然想到能不能用正方體代替demo中的球體粒子。我不禁被自己的想法嚇了一跳,球體的實現僅僅是簡單的畫圓,因為球體在任意角度任意距離的視圖都是圓(如果有視圖的話);而正方體有6個面8個點12條線,在canvas上的渲染多了n個數量級。先不說性能的問題,單單要實現六個面的旋轉和繪制就不是一件特別容易的事情。

  說干就干,經過曲折的過程,終於得到了一個半成品 -> 粒子化之正方體

  

事情的經過

  事情的經過絕不像得到的結果那樣簡單。雖然半成品demo在視覺上還有些許違和感,但已經能基本上達到我對粒子化特效的要求了。

  那么接下來說說我這次的蛋疼經歷吧。

  之前我們已經實現了一個點在三維系的坐標轉換(如不懂,可參考 rotate 3d基礎),並且得到了這樣的一個demo -> 3d球體。 那么我想,既然能得到點在三維系的空間轉換坐標,根據點-線-面的原理,理論上應該很容易實現正方體在三維系的體現,不就是初始化相對位置一定的8個點么?而且之前也簡單地實現了一個面的demo -> 3d愛心,當時認為並不難。

  於是我根據一定的相對位置,在三維系中初始化了8個點,每幀渲染的同時實現8個點的位置轉移,並且根據8個點的位置每幀重繪12條線,得到demo -> 3d正方體

  似乎很順利,接着給6個面上色,效果圖如下:

  這時我意識到應該是面的繪制順序出錯了,在每幀的繪制前應該先給面排個序,比如圖示的正方體的體心是三維系的原點,那么正方體的后面肯定是不可見的,所以應該先繪制。而在制作三維球體旋轉時,是根據球體中心在三維系的坐標z值排序的,這一點也很好理解,越遠的越容易被擋就越先畫嘛;同時我在WAxes的這篇用Canvas玩3D:點-線-面中看到他繪制正方體的方法是根據6個面中心點的z值進行排序,乍一想似乎理所當然,於是我去實現了,體心在原點體驗良好,demo -> 3d正方體,但是體心一改變位置,就坑爹了...

  

  圖示的正方體體心在原點的右側(沿x軸正方向),但是畫出來的正方體卻有違和感,為何?接着我還原了繪制的過程:

                 

  繪制過程先繪制了正方體的左面,再繪制了上面,而根據生活經驗這兩個面的繪制順序應該是先上面,再左面!不斷的尋找錯誤,我發現這兩個面中點的z值是一樣的,甚至除了前后兩個面,其他的四個面的z值都是一樣的,也就是說這個例子中后面最先繪,前面最后繪,其他四個面的繪制順序是任意的。我繼續朝着這個方向前進,根據我的生活經驗,如果像上圖一樣體心在原點右邊(其實應該是視點,當時認為是原點),那么如果面的z值相同,應該根據面與原點的x方向的距離進行排序,畢竟距離小的先看到,如果x方向距離又相同,那么根據y方向的距離進行排序,代碼如下:

  

var that = this;
this.f.sort(function (a, b) {
  if(b.zIndex !== a.zIndex)
    return b.zIndex - a.zIndex;
  else if(b.xIndex !== a.xIndex) {
    // 觀察基准點(0,0,0)
    if(that.x >= 0)
      return b.xIndex - a.xIndex;
    else 
      return a.xIndex - b.xIndex;
  } else {
    if(that.y >= 0)
      return b.yIndex - a.yIndex;
    else
      return a.yIndex - b.yIndex;
  }

  因為排序中this指向了window,還需賦值給一個另外的變量保存。事情似乎在此能畫上一個圓滿的句號,but...

  調整后繼續出現違和感(截圖如下),雖然違和感的體驗就在那么一瞬,但是我還是覺得是不是這個排序思路出錯了?於是進一步驗證,通過調試,將面的排序結果和正確的繪制順序作對比,最終發現排序算法是錯誤的,最后知道真相的我眼淚掉下來。

       

  於是在知乎上問了下:怎樣在二維上確定一個三維空間正方體六個面的繪制順序? 有計算機圖形學基礎的請無視。

  原來這是一個古老的問題,在各位圖形學大大的眼里是很基礎的問題了。原來這個問題稱為隱藏表面消除問題。

      

  然后我跟着這個方法進行了繪制,一開始把視點和原點搞混掉了。也就是判斷每個面的法向量(不取指向體心的那條)和面(近似取面中心)到視點的那條向量之間的角度,如果小於90度則是可見。想了一下,似乎還真是那么一回事。然后需要設定視點的坐標,隨意設置,只要合乎常理就行,這里我設置了(0,0,-500),在z方向肯定是個負值。

  一個正方體差不多搞定了,多個正方體呢?問題又出現:

  很顯然,正方體之間也有繪制的先后順序,這里粗略地采用根據體心排序的方法,按照Milo Yip的說法,這可以解決大部分情況,但也會漏掉一些最壞情況。最好的做法是zbuffer算法。

  於是乎,一個多正方體demo新鮮出爐了-> 多正方體demo

  如果要打造 粒子化之正方體 的效果,參考-> 談談文字圖片粒子化 I

  這里我設置了場景(Garden)、正方體(Cube)、面(Face)、點(Ball)四個類。

  梳理一下多個正方體具體渲染過程:

  • 先將正方體進行排序,確定正方體的繪制順序
  • 接着渲染每個正方體,先渲染正方體的各個點,改變各個點最新的坐標
for(var i = 0; i < 8; i++) 
  this.p[i].render();
  • 點渲染完后,根據最新的點的坐標調整正方體體心坐標,為下一幀的正方體排序准備
this.changeCoordinate();
  • 獲取每個面法向量和面中點和視點夾角cos值,如果大於0(夾角小於90)則繪制(這里其實不用排序):
for(var i = 0; i < 6; i++)
  this.f[i].angle = this.f[i].getAngle();

this.f.sort(function (a, b) {
  return a.angle > b.angle;
});

for(var i = 0; i < 6; i++) {
  // 夾角 < 90,繪制
  if(this.f[i].angle > 0)
    this.f[i].draw();
}
  • 反復渲染

  完整代碼如下:

  1 <!DOCTYPE html>
  2 <html>
  3   <head>
  4     <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
  5     <title> rotate 3d</title>
  6     <script>
  7       window.onload = function() {
  8         var canvas = document.getElementById('canvas');
  9         var ctx = canvas.getContext('2d');
 10         // var img = document.getElementById('img1');
 11         // ctx.drawImage(img, 0, 0);
 12         // var data = ctx.getImageData(0, 0, canvas.width, canvas.height).data;
 13         // ctx.clearRect(0, 0, canvas.width, canvas.height);
 14         // var length = data.length;
 15         // var num = 0;
 16         // var textPoint = [];
 17         // var r = 5;
 18         // var offsetX = -130;
 19         // var offsetY = -170;
 20         // for (var i = 0, wl = canvas.width * 4; i < length; i += 4) {
 21         //   if (data[i + 3]) {
 22         //     var x = (i % wl) / 4;
 23         //     var y = parseInt(i / wl)
 24         //     num++;
 25         //     textPoint.push([offsetX + x * r * 2, offsetY + y * r * 2]);
 26         //   }
 27         // }
 28         
 29         var garden = new Garden(canvas);
 30 
 31         // 設置二維視角原點(一般為畫布中心)
 32         garden.setBasePoint(500, 250);
 33         // for(var i = 0; i < textPoint.length; i++)
 34         //   garden.createCube(textPoint[i][0], textPoint[i][1], 0, r - 1);
 35  
 36         // 構造
 37         var z = 20;
 38         garden.createCube(0, 0, z, 30);
 39         garden.createCube(60, 0, z, 20);
 40         garden.createCube(-60, 0, z, 20);
 41 
 42         garden.createCube(0, 60, z, 20);
 43         garden.createCube(60, 60, z, 20);
 44         garden.createCube(-60, 60, z, 20);
 45         garden.createCube(60, -60, z, 20);
 46         garden.createCube(0, -60, z, 20);
 47         
 48         garden.createCube(-60, -60, z, 20);
 49 
 50 
 51         // 設置監聽
 52         // garden.setListener();
 53 
 54         // 渲染
 55         setInterval(function() {garden.render();}, 1000 / 60);  
 56       };
 57 
 58       function Garden(canvas) {
 59         this.canvas = canvas;
 60         this.ctx = this.canvas.getContext('2d');
 61 
 62         // 三維系在二維上的原點
 63         this.vpx = undefined;
 64         this.vpy = undefined;
 65         this.cubes = [];
 66         this.angleY = Math.PI / 180 * 1;
 67         this.angleX = Math.PI / 180 * 1;
 68       }
 69 
 70       Garden.prototype = {
 71         setBasePoint: function(x, y) {
 72           this.vpx = x;
 73           this.vpy = y;
 74         },
 75 
 76         createCube: function(x, y, z, r) {
 77           this.cubes.push(new Cube(this, x, y, z, r));
 78         },
 79 
 80         render: function() {
 81           this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
 82           // var that = this;
 83           this.cubes.sort(function (a, b) {
 84           if(b.z !== a.z)
 85             return b.z - a.z;
 86           else if(b.x !== a.x) {
 87             if(b.x >= 0 && a.x >= 0 || b.x <= 0 && a.x <= 0)
 88               return Math.abs(b.x) - Math.abs(a.x);
 89             else return b.x - a.x;
 90           } else {
 91             if(b.y >= 0 && a.y >= 0 || b.y <= 0 && a.y <= 0)
 92               return Math.abs(b.y) - Math.abs(a.y);
 93             else return b.y - a.y;
 94           }
 95         });
 96 
 97           for(var i = 0; i < this.cubes.length; i++) 
 98             this.cubes[i].render();
 99         }
100 
101         // setListener: function() {
102         //   var that = this;
103         //   document.addEventListener('mousemove', function(event){
104         //     var x = event.clientX - that.vpx;
105         //     var y = event.clientY - that.vpy;
106         //     that.angleY = -x * 0.0001;
107         //     that.angleX = y * 0.0001;
108         //   });
109         // }
110       };
111 
112       function Ball(cube, x, y, z) {
113         this.cube = cube;
114 
115         // 三維上坐標
116         this.x = x;
117         this.y = y;
118         this.z = z;
119 
120         // 二維上坐標
121         this.x2 = undefined;
122         this.y2 = undefined;
123       }
124       
125       Ball.prototype = {
126         // 繞y軸變化,得出新的x,z坐標
127         rotateY: function() {
128           var cosy = Math.cos(this.cube.angleY);
129           var siny = Math.sin(this.cube.angleY);
130           var x1 = this.z * siny + this.x * cosy;
131           var z1 = this.z * cosy - this.x * siny;
132           this.x = x1;
133           this.z = z1;
134         },
135 
136         // 繞x軸變化,得出新的y,z坐標
137         rotateX: function() {
138           var cosx = Math.cos(this.cube.angleX);
139           var sinx = Math.sin(this.cube.angleX);
140           var y1 = this.y * cosx - this.z * sinx;
141           var z1 = this.y * sinx + this.z * cosx;
142           this.y = y1;
143           this.z = z1;
144         },
145 
146         getPositionInTwoDimensionalSystem: function(a) {
147           // focalLength 表示當前焦距,一般可設為一個常量
148           var focalLength = 300; 
149           // 把z方向扁平化
150           var scale = focalLength / (focalLength + this.z);
151           this.x2 = this.cube.garden.vpx + this.x * scale;
152           this.y2 = this.cube.garden.vpy + this.y * scale;
153         },
154 
155         render: function() {
156           this.rotateX();
157           this.rotateY();
158           this.getPositionInTwoDimensionalSystem();
159         }
160       };
161 
162       function Cube(garden, x, y, z, r) {
163         this.garden = garden;
164 
165         // 正方體中心和半徑
166         this.x = x;
167         this.y = y;
168         this.z = z;
169         this.r = r;
170 
171         this.angleX = Math.PI / 180 * 1;
172         this.angleY = Math.PI / 180 * 1;
173 
174         // cube的8個點
175         this.p = [];
176 
177         // cube的6個面
178         this.f = [];
179 
180         this.init();
181       }
182 
183       Cube.prototype = {
184         init: function() {
185           // 正方體的每個頂點都是一個ball類實現
186           this.p[0] = new Ball(this, this.x - this.r, this.y - this.r, this.z - this.r);
187           this.p[1] = new Ball(this, this.x - this.r, this.y + this.r, this.z - this.r);
188           this.p[2] = new Ball(this, this.x + this.r, this.y + this.r, this.z - this.r);
189           this.p[3] = new Ball(this, this.x + this.r, this.y - this.r, this.z - this.r);
190           this.p[4] = new Ball(this, this.x - this.r, this.y - this.r, this.z + this.r);
191           this.p[5] = new Ball(this, this.x - this.r, this.y + this.r, this.z + this.r);
192           this.p[6] = new Ball(this, this.x + this.r, this.y + this.r, this.z + this.r);
193           this.p[7] = new Ball(this, this.x + this.r, this.y - this.r, this.z + this.r);
194 
195           // 正方體6個面
196           this.f[0] = new Face(this, this.p[0], this.p[1], this.p[2], this.p[3]);
197           this.f[1] = new Face(this, this.p[3], this.p[2], this.p[6], this.p[7]);
198           this.f[2] = new Face(this, this.p[4], this.p[5], this.p[6], this.p[7]);
199           this.f[3] = new Face(this, this.p[4], this.p[5], this.p[1], this.p[0]);
200           this.f[4] = new Face(this, this.p[0], this.p[3], this.p[7], this.p[4]);
201           this.f[5] = new Face(this, this.p[5], this.p[1], this.p[2], this.p[6]);
202         },
203 
204         render: function() {
205           for(var i = 0; i < 8; i++) 
206             this.p[i].render();
207 
208           // 八個點的坐標改變完后,改變cube體心坐標,為下一幀cube的排序作准備
209           this.changeCoordinate();
210 
211           for(var i = 0; i < 6; i++)
212             this.f[i].angle = this.f[i].getAngle();
213 
214           // 不是必須
215           this.f.sort(function (a, b) {
216             return a.angle > b.angle;
217           });
218 
219           for(var i = 0; i < 6; i++) {
220             // 夾角 < 90,繪制
221             if(this.f[i].angle > 0)
222               this.f[i].draw();
223           }
224         },
225 
226         // cube體心坐標改變
227         changeCoordinate: function() {
228           this.x = this.y = this.z = 0;
229           for(var i = 0; i < 8; i++) {
230             this.x += this.p[i].x;
231             this.y += this.p[i].y;
232             this.z += this.p[i].z;
233           }
234           this.x /= 8;
235           this.y /= 8;
236           this.z /= 8;
237         }
238       };
239 
240       function Face(cube, a, b, c, d) {
241         this.cube = cube;
242         this.a = a;
243         this.b = b;
244         this.c = c;
245         this.d = d;
246         this.color = '#' + ('00000' + parseInt(Math.random() * 0xffffff).toString(16)).slice(-6);
247         // 面的法向量和面心到視點向量的夾角的cos值
248         this.angle = undefined;
249       }
250 
251       Face.prototype = {
252         draw: function() {
253           var ctx = this.cube.garden.ctx;
254           ctx.beginPath();
255           ctx.fillStyle = this.color;
256           ctx.moveTo(this.a.x2, this.a.y2);
257           ctx.lineTo(this.b.x2, this.b.y2);
258           ctx.lineTo(this.c.x2, this.c.y2);
259           ctx.lineTo(this.d.x2, this.d.y2);
260           ctx.closePath();
261           ctx.fill();
262         },
263 
264         // 獲取面的法向量和z軸夾角
265         getAngle: function() {
266           var x = (this.a.x + this.b.x + this.c.x + this.d.x) / 4 - this.cube.x;
267           var y = (this.a.y + this.b.y + this.c.y + this.d.y) / 4 - this.cube.y;
268           var z = (this.a.z + this.b.z + this.c.z + this.d.z) / 4 - this.cube.z;
269           // 面的法向量
270           var v = new Vector(x, y, z);
271 
272           // 視點設為(0,0,-500)
273           var x = 0 - (this.a.x + this.b.x + this.c.x + this.d.x) / 4;
274           var y = 0  - (this.a.y + this.b.y + this.c.y + this.d.y) / 4;
275           var z = - 500 - (this.a.z + this.b.z + this.c.z + this.d.z) / 4;
276           // 面心指向視點的向量
277           var v2 = new Vector(x, y, z);
278           return v.dot(v2);
279         }
280       };  
281 
282       function Vector(x, y, z) {
283         this.x = x;
284         this.y = y;
285         this.z = z;
286       }    
287 
288       // 向量點積,大於0為0~90度
289       Vector.prototype.dot = function(v) {
290         return this.x * v.x + this.y * v.y + this.z * v.z;
291       }
292       
293     </script>
294   </head>
295   <body bgcolor='#000'> 
296     <canvas id='canvas' width=1000 height=600 style='background-color:#000'>
297       This browser does not support html5.
298     </canvas>
299   </body>
300 </html>
View Code

 

  總之這樣的操作正方體之間的遮掩順序還是會出現錯誤的,比如下圖:

   

  ps,這是在其他地方看到的判斷函數,占位,備用:

事情的結果

  事情似乎得到了一個較為滿意的結果。如果正方體面沒有緊緊相鄰,體驗效果還是不錯的。(緊緊相交會出現閃動)

  事實上,因為canvas暫時只支持2d,所以3d的渲染如果要得到最好的效果還是要使用webGL,但是這個思考的過程還是很重要的。

  That's all.


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM