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

3D炫酷放烟花效果

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

粒子范围

主要是源码中的rasterizePoint方法:

function rasterizePoint(x, y, z) {
    var p, d;
    x -= playerX;
    y -= playerY;
    z -= playerZ;
    p = Math.atan2(x, z);
    d = Math.sqrt(x * x + z * z);
    x = Math.sin(p - yaw) * d;
    z = Math.cos(p - yaw) * d;
    p = Math.atan2(y, z);
    d = Math.sqrt(y * y + z * z);
    y = Math.sin(p - pitch) * d;
    z = Math.cos(p - pitch) * d;
    p = Math.atan2(x, y);
    d = Math.sqrt(x * x + y * y);
    x = Math.sin(p - roll) * d;
    y = Math.cos(p - roll) * d;
    var rx1 = -1000,
        ry1 = 1,
        rx2 = 1000,
        ry2 = 1,
        rx3 = 0,
        ry3 = 0,
        rx4 = x,
        ry4 = z,
        uc = (ry4 - ry3) * (rx2 - rx1) - (rx4 - rx3) * (ry2 - ry1);
    if(!uc) return {
        x: 0,
        y: 0,
        d: -1
    };
    var ua = ((rx4 - rx3) * (ry1 - ry3) - (ry4 - ry3) * (rx1 - rx3)) / uc;
    var ub = ((rx2 - rx1) * (ry1 - ry3) - (ry2 - ry1) * (rx1 - rx3)) / uc;
    //    console.log(ua,ub,uc);
    if(!z) z = .000000001;
    //        console.log(ua*(rx2-rx1))
    if(ua > 0 && ua < 1 && ub > 0 && ub < 1) {
        return {
            x: cx + (rx1 + ua * (rx2 - rx1)) * scale,
            y: cy + y / z * scale,
            d: Math.sqrt(x * x + y * y + z * z)
        };
    } else {
        return {
            d: -1
        };
    }
}

这个方法比较难理解,它的作用主要是将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球:

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

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

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

烟花尾巴效果

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

for(i = 0; i < sparks.length; ++i) {
    if(sparks[i].alpha > 0 && sparks[i].radius > 5) {
        sparks[i].alpha -= .01;
        sparks[i].radius /= 1.02;
        sparks[i].vy += gravity;
        var point = {};
        point.x = sparks[i].x;
        point.y = sparks[i].y;
        point.z = sparks[i].z;
        if(sparks[i].trail.length) {
            x = sparks[i].trail[sparks[i].trail.length - 1].x;
            y = sparks[i].trail[sparks[i].trail.length - 1].y;
            z = sparks[i].trail[sparks[i].trail.length - 1].z;
            d = ((point.x - x) * (point.x - x) + (point.y - y) * (point.y - y) + (point.z - z) * (point.z - z));
            if(d > 20) {
                sparks[i].trail.push(point);
            }
        } else {
            sparks[i].trail.push(point);
        }
        if(sparks[i].trail.length > 5) sparks[i].trail.splice(0, 1);
        sparks[i].x += sparks[i].vx;
        sparks[i].y += sparks[i].vy;
        sparks[i].z += sparks[i].vz;
        sparks[i].vx /= 1.075;
        sparks[i].vy /= 1.075;
        sparks[i].vz /= 1.075;
    } else {
        sparks.splice(i, 1);
    }
}

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

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

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

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

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

回到顶部
我要评论

所有评论

返回
邮箱:
绑定
取消
×

我要评论

回复:

昵称:(昵称不超过20个字)

图片:

邮箱:
绑定邮箱后,若有回复,会邮件通知。
提交
还可以输入500个字