事情的起因

  之前写了篇谈谈文字图片粒子化 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();
}
  • 反复渲染

  完整代码如下:

 <!DOCTYPE html>
 <html>
   <head>
     <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
     <title> rotate 3d</title>
     <script>
       window.onload = function() {
         var canvas = document.getElementById('canvas');
         var ctx = canvas.getContext('2d');
         // var img = document.getElementById('img1');
         // ctx.drawImage(img, 0, 0);
         // var data = ctx.getImageData(0, 0, canvas.width, canvas.height).data;
         // ctx.clearRect(0, 0, canvas.width, canvas.height);
         // var length = data.length;
         // var num = 0;
         // var textPoint = [];
         // var r = 5;
         // var offsetX = -130;
         // var offsetY = -170;
         // for (var i = 0, wl = canvas.width * 4; i < length; i += 4) {
         //   if (data[i + 3]) {
         //     var x = (i % wl) / 4;
         //     var y = parseInt(i / wl)
         //     num++;
         //     textPoint.push([offsetX + x * r * 2, offsetY + y * r * 2]);
         //   }
         // }

         var garden = new Garden(canvas);

         // 设置二维视角原点(一般为画布中心)
         garden.setBasePoint(500, 250);
         // for(var i = 0; i < textPoint.length; i++)
         //   garden.createCube(textPoint[i][0], textPoint[i][1], 0, r - 1);

         // 构造
         var z = 20;
         garden.createCube(0, 0, z, 30);
         garden.createCube(60, 0, z, 20);
         garden.createCube(-60, 0, z, 20);

         garden.createCube(0, 60, z, 20);
         garden.createCube(60, 60, z, 20);
         garden.createCube(-60, 60, z, 20);
         garden.createCube(60, -60, z, 20);
         garden.createCube(0, -60, z, 20);

         garden.createCube(-60, -60, z, 20);

         // 设置监听
         // garden.setListener();

         // 渲染
         setInterval(function() {garden.render();}, 1000 / 60);
       };

       function Garden(canvas) {
         this.canvas = canvas;
         this.ctx = this.canvas.getContext('2d');

         // 三维系在二维上的原点
         this.vpx = undefined;
         this.vpy = undefined;
         this.cubes = [];
         this.angleY = Math.PI / 180 * 1;
         this.angleX = Math.PI / 180 * 1;
       }

       Garden.prototype = {
         setBasePoint: function(x, y) {
           this.vpx = x;
           this.vpy = y;
         },

         createCube: function(x, y, z, r) {
           this.cubes.push(new Cube(this, x, y, z, r));
         },

         render: function() {
           this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
           // var that = this;
           this.cubes.sort(function (a, b) {
           if(b.z !== a.z)
             return b.z - a.z;
           else if(b.x !== a.x) {
             if(b.x >= 0 && a.x >= 0 || b.x <= 0 && a.x <= 0)
               return Math.abs(b.x) - Math.abs(a.x);
             else return b.x - a.x;
           } else {
             if(b.y >= 0 && a.y >= 0 || b.y <= 0 && a.y <= 0)
               return Math.abs(b.y) - Math.abs(a.y);
             else return b.y - a.y;
           }
         });

           for(var i = 0; i < this.cubes.length; i++)
             this.cubes[i].render();
         }

         // setListener: function() {
         //   var that = this;
         //   document.addEventListener('mousemove', function(event){
         //     var x = event.clientX - that.vpx;
         //     var y = event.clientY - that.vpy;
         //     that.angleY = -x * 0.0001;
         //     that.angleX = y * 0.0001;
         //   });
         // }
       };

       function Ball(cube, x, y, z) {
         this.cube = cube;

         // 三维上坐标
         this.x = x;
         this.y = y;
         this.z = z;

         // 二维上坐标
         this.x2 = undefined;
         this.y2 = undefined;
       }

       Ball.prototype = {
         // 绕y轴变化,得出新的x,z坐标
         rotateY: function() {
           var cosy = Math.cos(this.cube.angleY);
           var siny = Math.sin(this.cube.angleY);
           var x1 = this.z * siny + this.x * cosy;
           var z1 = this.z * cosy - this.x * siny;
           this.x = x1;
           this.z = z1;
         },

         // 绕x轴变化,得出新的y,z坐标
         rotateX: function() {
           var cosx = Math.cos(this.cube.angleX);
           var sinx = Math.sin(this.cube.angleX);
           var y1 = this.y * cosx - this.z * sinx;
           var z1 = this.y * sinx + this.z * cosx;
           this.y = y1;
           this.z = z1;
         },

         getPositionInTwoDimensionalSystem: function(a) {
           // focalLength 表示当前焦距,一般可设为一个常量
           var focalLength = 300;
           // 把z方向扁平化
           var scale = focalLength / (focalLength + this.z);
           this.x2 = this.cube.garden.vpx + this.x * scale;
           this.y2 = this.cube.garden.vpy + this.y * scale;
         },

         render: function() {
           this.rotateX();
           this.rotateY();
           this.getPositionInTwoDimensionalSystem();
         }
       };

       function Cube(garden, x, y, z, r) {
         this.garden = garden;

         // 正方体中心和半径
         this.x = x;
         this.y = y;
         this.z = z;
         this.r = r;

         this.angleX = Math.PI / 180 * 1;
         this.angleY = Math.PI / 180 * 1;

         // cube的8个点
         this.p = [];

         // cube的6个面
         this.f = [];

         this.init();
       }

       Cube.prototype = {
         init: function() {
           // 正方体的每个顶点都是一个ball类实现
           this.p[0] = new Ball(this, this.x - this.r, this.y - this.r, this.z - this.r);
           this.p[1] = new Ball(this, this.x - this.r, this.y + this.r, this.z - this.r);
           this.p[2] = new Ball(this, this.x + this.r, this.y + this.r, this.z - this.r);
           this.p[3] = new Ball(this, this.x + this.r, this.y - this.r, this.z - this.r);
           this.p[4] = new Ball(this, this.x - this.r, this.y - this.r, this.z + this.r);
           this.p[5] = new Ball(this, this.x - this.r, this.y + this.r, this.z + this.r);
           this.p[6] = new Ball(this, this.x + this.r, this.y + this.r, this.z + this.r);
           this.p[7] = new Ball(this, this.x + this.r, this.y - this.r, this.z + this.r);

           // 正方体6个面
           this.f[0] = new Face(this, this.p[0], this.p[1], this.p[2], this.p[3]);
           this.f[1] = new Face(this, this.p[3], this.p[2], this.p[6], this.p[7]);
           this.f[2] = new Face(this, this.p[4], this.p[5], this.p[6], this.p[7]);
           this.f[3] = new Face(this, this.p[4], this.p[5], this.p[1], this.p[0]);
           this.f[4] = new Face(this, this.p[0], this.p[3], this.p[7], this.p[4]);
           this.f[5] = new Face(this, this.p[5], this.p[1], this.p[2], this.p[6]);
         },

         render: function() {
           for(var i = 0; i < 8; i++)
             this.p[i].render();

           // 八个点的坐标改变完后,改变cube体心坐标,为下一帧cube的排序作准备
           this.changeCoordinate();

           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();
           }
         },

         // cube体心坐标改变
         changeCoordinate: function() {
           this.x = this.y = this.z = 0;
           for(var i = 0; i < 8; i++) {
             this.x += this.p[i].x;
             this.y += this.p[i].y;
             this.z += this.p[i].z;
           }
           this.x /= 8;
           this.y /= 8;
           this.z /= 8;
         }
       };

       function Face(cube, a, b, c, d) {
         this.cube = cube;
         this.a = a;
         this.b = b;
         this.c = c;
         this.d = d;
         this.color = '#' + ('00000' + parseInt(Math.random() * 0xffffff).toString(16)).slice(-6);
         // 面的法向量和面心到视点向量的夹角的cos值
         this.angle = undefined;
       }

       Face.prototype = {
         draw: function() {
           var ctx = this.cube.garden.ctx;
           ctx.beginPath();
           ctx.fillStyle = this.color;
           ctx.moveTo(this.a.x2, this.a.y2);
           ctx.lineTo(this.b.x2, this.b.y2);
           ctx.lineTo(this.c.x2, this.c.y2);
           ctx.lineTo(this.d.x2, this.d.y2);
           ctx.closePath();
           ctx.fill();
         },

         // 获取面的法向量和z轴夹角
         getAngle: function() {
           var x = (this.a.x + this.b.x + this.c.x + this.d.x) / 4 - this.cube.x;
           var y = (this.a.y + this.b.y + this.c.y + this.d.y) / 4 - this.cube.y;
           var z = (this.a.z + this.b.z + this.c.z + this.d.z) / 4 - this.cube.z;
           // 面的法向量
           var v = new Vector(x, y, z);

           // 视点设为(0,0,-500)
           var x = 0 - (this.a.x + this.b.x + this.c.x + this.d.x) / 4;
           var y = 0  - (this.a.y + this.b.y + this.c.y + this.d.y) / 4;
           var z = - 500 - (this.a.z + this.b.z + this.c.z + this.d.z) / 4;
           // 面心指向视点的向量
           var v2 = new Vector(x, y, z);
           return v.dot(v2);
         }
       };  

       function Vector(x, y, z) {
         this.x = x;
         this.y = y;
         this.z = z;
       }    

       // 向量点积,大于0为0~90度
       Vector.prototype.dot = function(v) {
         return this.x * v.x + this.y * v.y + this.z * v.z;
       }

     </script>
   </head>
   <body bgcolor='#000'>
     <canvas id='canvas' width=1000 height=600 style='background-color:#000'>
       This browser does not support html5.
     </canvas>
   </body>
 </html>

  总之这样的操作正方体之间的遮掩顺序还是会出现错误的,比如下图:

  

  ps,这是在其他地方看到的判断函数,占位,备用:

事情的结果

  事情似乎得到了一个较为满意的结果。如果正方体面没有紧紧相邻,体验效果还是不错的。(紧紧相交会出现闪动)

  事实上,因为canvas暂时只支持2d,所以3d的渲染如果要得到最好的效果还是要使用webGL,但是这个思考的过程还是很重要的。

  That's all.

Canvas之蛋疼的正方体绘制体验的更多相关文章

  1. Canvas+Video打造酷炫播放体验

    一.简介 直到现在,仍然不存在一项旨在网页上显示视频的标准. 今天,大多数视频是通过插件(比如 Flash)来显示的.然而,并非所有浏览器都拥有同样的插件. HTML5 规定了一种通过 video 元 ...

  2. 【自己给自己题目做】:如何在Canvas上实现魔方效果

    最终demo -> 3d魔方 体验方法: 浮动鼠标找到合适的位置,按空格键暂停 选择要翻转的3*3模块,找到相邻两个正方体,鼠标点击第一个正方体,并且一直保持鼠标按下的状态直到移到第二个正方体后 ...

  3. 【自己给自己题目做】:如何用裸的 Canvas 实现魔方效果

    最终demo -> 3d魔方 体验方法: 浮动鼠标找到合适的位置,按空格键暂停 选择要翻转的3*3模块,找到相邻两个正方体,鼠标点击第一个正方体,并且一直保持鼠标按下的状态直到移到第二个正方体后 ...

  4. 谈谈UI架构设计的演化

    谈谈UI架构设计的演化 经典MVC 在1979年,经典MVC模式被提出. 在当时,人们一直试图将纯粹描述思维中的对象与跟计算机环境打交道的代码隔离开来,而Trygve Reenskaug在跟一些人的讨 ...

  5. Php开发完全跨站点跨域名单点(SSO)同步登录和注销

    From:http://www.cnblogs.com/JinkoWu/p/5056646.html 先来说说什么是单点登录(SSO).来自百科的介绍:SSO英文全称Single Sign On,单点 ...

  6. 完全跨站点跨域名单点(SSO)同步登录和注销

    先来说说什么是单点登录(SSO).来自百科的介绍:SSO英文全称Single Sign On,单点登录.SSO是在多个应用系统中,用户只需要登录一次就可以访问所有相互信任的应用系统.它包括可以将这次主 ...

  7. File API文件操作之FileReader二

    上一篇说了FileAPI中FileReader的readAsText,这里继续上文,说说另外一个API readAsDataURL. 这个接口是将File或者Blob读成base64格式的字符串,然后 ...

  8. GraphicsMagick 学习笔记

    两种最常用的图片处理工具:GraphicsMagick 或 ImageMagick,GM是IM的分支,这两个图片处理工具功能基本相同,各有特色.但他们并不是nodejs的插件,它们都是客户端软件,li ...

  9. canvas初体验之加载图片

    上一篇的介绍主要是画一些基本的图案,这一篇主要是加载图案. canvas加载图片主要分为两个步骤: 1.获取图片资源. 2.将图片资源画到画布上. 1.1获取图片资源,canvasAPI为我们提供了多 ...

随机推荐

  1. Java Gradle入门指南之内建与定制任务类(buildSrc、Groovy等)

        上一篇随笔介绍了Gradle的安装与任务管理,这篇着重介绍Gradle的内建任务(in-built tasks)与自定义任务(custom tasks),借助Gradle提供的众多内建任务类型 ...

  2. 使用Web.Config Transformation配置灵活的配置文件

    发布Asp.net程序的时候,开发环境和发布环境的Web.Config往往不同,比如connectionstring等.如果常常有发布的需求,就需要常常修改web.config文件,这往往是一件非常麻 ...

  3. Zookeeper 服务注册和发现

    Zookeeper 分布式服务框架是 Apache Hadoop 的一个子项目,它主要是用来解决分布式应用中经常遇到的一些数据管理问题,如:统一命名服务.状态同步服务.集群管理.分布式应用配置项的管理 ...

  4. 微信公众平台开发(三) 订阅事件(subscribe)处理

    一.简介 新用户关注微信公众平台,将产生一个订阅事件,即subscribe事件,默认代码中没有对这一事件进行相应回复处理. 在新用户关注公众平台后,可能想知道该平台提供了哪些功能,以及怎样使用该平台, ...

  5. Linux学习--------一

    用户不能直接操作Kemel,所以需要通过Shell来操作Kemel(内核) Shell 分为CLI与GUI两种 CLI:Command Line Interface GUI:Graphical Use ...

  6. Linux系统管理命令之用户管理

    1.添加用户useradd   2.删除用户userdel userdel aming 彻底删除用户(包括删除用户目录) userdel -r aming 3.用户修改usermod    

  7. day 2 常用快捷键

    tab命令或路径补全**,linux里最有用的快捷键,如果tab不到路径或命令,就代表没有这个路径或者命令,还有可能是权限不对. ctrl+c :终止当前任务命令或程序. ctrl+d 退出当前用户环 ...

  8. setTimeout()与setInterval()——走马灯效果

    JavaScript中的setTimeout()与setInterval()都是指延时执行某一操作. 但setInterval()指每隔指定时间执行某操作,会循环不断地执行该操作:setTimeout ...

  9. Qt model和tableview的使用

    QT中的model和tableview都是采用index索引   index含有两个成员变量一个是row   一个是column  对应该索引的行号.列号 model提供数据    view提供视图  ...

  10. 彻底解决mysql中文乱码的办法,修改mysql解压缩版(免安装版或zip版)字符编码

    MySQL会出现中文乱码的原因不外乎下列几点:1.server本身设定问题,例如server字符编码还停留在latin12.table的语系设定问题(包含character与collation)3.客 ...