zoukankan      html  css  js  c++  java
  • 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.

  • 相关阅读:
    .NET XmlNavigator with Namespace
    编程要素
    【FOJ】1962 新击鼓传花游戏
    【POJ】1389 Area of Simple Polygons
    【POJ】2482 Stars in Your Window
    【HDU】3265 Posters
    【HDU】1199 Color the Ball
    【HDU】3642 Get The Treasury
    【HDU】4027 Can you answer these queries?
    【HDU】1542 Atlantis
  • 原文地址:https://www.cnblogs.com/lessfish/p/4258977.html
Copyright © 2011-2022 走看看