最近看到一个炫酷的放烟花效果:

    3D炫酷放烟花效果

    源码也并不是那么容易看懂,下面记录了一些源码中的难点:

    粒子范围

    主要是源码中的rasterizePoint方法:

    1. function rasterizePoint(x, y, z) {
    2.     var p, d;
    3.     x -= playerX;
    4.     y -= playerY;
    5.     z -= playerZ;
    6.     p = Math.atan2(x, z);
    7.     d = Math.sqrt(x * x + z * z);
    8.     x = Math.sin(p - yaw) * d;
    9.     z = Math.cos(p - yaw) * d;
    10.     p = Math.atan2(y, z);
    11.     d = Math.sqrt(y * y + z * z);
    12.     y = Math.sin(p - pitch) * d;
    13.     z = Math.cos(p - pitch) * d;
    14.     p = Math.atan2(x, y);
    15.     d = Math.sqrt(x * x + y * y);
    16.     x = Math.sin(p - roll) * d;
    17.     y = Math.cos(p - roll) * d;
    18.     var rx1 = -1000,
    19.         ry1 = 1,
    20.         rx2 = 1000,
    21.         ry2 = 1,
    22.         rx3 = 0,
    23.         ry3 = 0,
    24.         rx4 = x,
    25.         ry4 = z,
    26.         uc = (ry4 - ry3) * (rx2 - rx1) - (rx4 - rx3) * (ry2 - ry1);
    27.     if(!uc) return {
    28.         x: 0,
    29.         y: 0,
    30.         d: -1
    31.     };
    32.     var ua = ((rx4 - rx3) * (ry1 - ry3) - (ry4 - ry3) * (rx1 - rx3)) / uc;
    33.     var ub = ((rx2 - rx1) * (ry1 - ry3) - (ry2 - ry1) * (rx1 - rx3)) / uc;
    34.     //    console.log(ua,ub,uc);
    35.     if(!z) z = .000000001;
    36.     //        console.log(ua*(rx2-rx1))
    37.     if(ua > 0 && ua < 1 && ub > 0 && ub < 1) {
    38.         return {
    39.             x: cx + (rx1 + ua * (rx2 - rx1)) * scale,
    40.             y: cy + y / z * scale,
    41.             d: Math.sqrt(x * x + y * y + z * z)
    42.         };
    43.     } else {
    44.         return {
    45.             d: -1
    46.         };
    47.     }
    48. }

    这个方法比较难理解,它的作用主要是将3D坐标展现在2D上,我们先看它的下半部分,就是从var rx1 = -1000,往后的代码,看看那个计算表达式是计算啥的,到底有啥作用?

    首先我们将等式中的ua,ub简化一下:

    简化等式

    ua>0&&ua<1,(线条的斜率都是K43),想表达的内容是第四个点(D),需要在CA,CB的射线范围内,我们先看个正常范围的:

    很明显可以看到(b1-b3)/(b1-b2)的范围是在[0,1]之间的。我们看看如果D超过了射线范围:

    我们可以看到如果D不在范围之内,那么ua将>1。

    ub>0&&ub<1,(线条的斜率都是K12),想表达的内容是第四个点(D),需要在AB射线之上,我们先看个正常范围的:

    我们看个在AB射线之下的:

    因此我们可以知道,ua>0&&ua<1,ub>0&&ub<1,是要让点的范围在指定范围,范围如下:

    转换后的坐标:rx1 + ua * (rx2 - rx1)则是根据ua的比例,将点展现在[rx1,rx2]这个范围内;其实我们可以理解成范围内的点都会投影到AB这个平面上(xz坐标是俯视图,实际上AB是一个平面)。

    player和yaw,pitch,roll

    这个是出现在rasterizePoint上半部分的内容,player代表一个观察者,具体有playerX,playerY,playerZ,代表这个观察者的三维坐标。

    yaw,pitch,roll是从航天飞机那边来的概念: yaw,pitch,roll,简单来说,yaw代表绕着y轴旋转;pitch代表绕着x轴旋转;roll代表绕着z轴旋转。

    我们看一下player配合yaw如何实现绕y轴旋转(方块代表生成的粒子范围,它的范围类似一个长方体,通过位移旋转得到最终投影结果):

    第一步将观察者放到中心位置,第二步将点旋转一个角度,这样旋转后的投影刚好就为三维物体的侧面。

    那么到底旋转多少度才能呢?代码中为p-yaw度(其中yaw=pi+a,yaw角度在doLogic方法中赋值):

    p = Math.atan2(x, z);它求的是对边/邻边对应的角度,也就是上图中的a,p。其中a为旋转前得到的结果(记录yaw=a+pi),通过第一步将粒子范围位移后,再次计算得到角度p,然后p-yaw就可以得到粒子范围的侧面投影。

    绘制3D球星烟花

    splode方法中有一段代码比较难懂,它的作用是用来绘制3D球:

    1. var p1 = pi * 2 * Math.random();
    2. var p2 = pi * Math.random();
    3. var v = sparkV * (1 + Math.random() / 6)
    4. spark.vx = Math.sin(p1) * Math.sin(p2) * v;
    5. spark.vz = Math.cos(p1) * Math.sin(p2) * v;
    6. spark.vy = Math.cos(p2) * v;

    先随机得到到一个三维速度V,然后将它在xy轴上分解,得到Vy和Vxz,然后将Vxz在x和z轴分解,分解后的点其实在分布在一个3D球上:

    其中第一个角度的范围在[0,pi],第二个角度的范围在[0,2*pi],直观的展示如下:

    烟花尾巴效果

    在播放烟花的时候,我们不断的放入烟花尾巴:

    1. for(i = 0; i < sparks.length; ++i) {
    2.     if(sparks[i].alpha > 0 && sparks[i].radius > 5) {
    3.         sparks[i].alpha -= .01;
    4.         sparks[i].radius /= 1.02;
    5.         sparks[i].vy += gravity;
    6.         var point = {};
    7.         point.x = sparks[i].x;
    8.         point.y = sparks[i].y;
    9.         point.z = sparks[i].z;
    10.         if(sparks[i].trail.length) {
    11.             x = sparks[i].trail[sparks[i].trail.length - 1].x;
    12.             y = sparks[i].trail[sparks[i].trail.length - 1].y;
    13.             z = sparks[i].trail[sparks[i].trail.length - 1].z;
    14.             d = ((point.x - x) * (point.x - x) + (point.y - y) * (point.y - y) + (point.z - z) * (point.z - z));
    15.             if(d > 20) {
    16.                 sparks[i].trail.push(point);
    17.             }
    18.         } else {
    19.             sparks[i].trail.push(point);
    20.         }
    21.         if(sparks[i].trail.length > 5) sparks[i].trail.splice(0, 1);
    22.         sparks[i].x += sparks[i].vx;
    23.         sparks[i].y += sparks[i].vy;
    24.         sparks[i].z += sparks[i].vz;
    25.         sparks[i].vx /= 1.075;
    26.         sparks[i].vy /= 1.075;
    27.         sparks[i].vz /= 1.075;
    28.     } else {
    29.         sparks.splice(i, 1);
    30.     }
    31. }

    我们可以看到,烟花尾巴其实就是烟花经过的点,尾巴数组最多可以保留5个,并且只有当尾巴之间的距离达到一定距离的时候才会保存。

    现在我们来看看绘制烟花尾巴:

    1. for(j = sparks[i].trail.length - 1; j >= 0; --j) {
    2.     point2 = rasterizePoint(sparks[i].trail[j].x, sparks[i].trail[j].y, sparks[i].trail[j].z);
    3.     if(point2.d != -1) {
    4.         ctx.globalAlpha = j / sparks[i].trail.length * sparks[i].alpha / 2;
    5.         ctx.beginPath();
    6.         ctx.moveTo(point1.x, point1.y);
    7.         ctx.lineWidth = 1 + sparks[i].radius * 10 / (sparks[i].trail.length - j) / (1 + point2.d);
    8.         ctx.lineTo(point2.x, point2.y);
    9.         ctx.stroke();
    10.         point1.x = point2.x;
    11.         point1.y = point2.y;
    12.     }
    13. }

    烟花尾巴绘制的整体逻辑是将尾巴数组中的点利用moveTo,lineTo连接起来,其中globalAlpha想表达越靠近后面的尾巴透明度越小。

    lineWidth中为啥要除以(1 + point2.d)?为的是实现线条粗细的3D效果,距离越远的尾巴,越细。(同时为了方式point.d为0,因此+1),这种除以(1+d)的在很多地方都有,都是为了实现大小,尺寸等的3D效果,远小近大。

    回到顶部
    我要评论

    所有评论

      相关文章