用程式碼構建星辰大海

螞蟻金服資料體驗技術發表於2017-11-20

作者簡介 wuyue 螞蟻金服·資料體驗技術團隊

我們的征途是星辰大海
—阿里人的宣言

如何用程式碼構建阿里的星辰大海?
首先我們想象一下當前發生的一個場景, 漫天繁星閃爍,不時有流星劃破天際, 地球在自轉, 無數的人在不同的地點因為阿里提供的便捷服務在交易。

效果如下:

結構分解

如何構建上面的場景呢, 分解得到如下結構:

用程式碼構建星辰大海

星空

首先要有一片天, 一個矩形,給黑色背景就好了。

用程式碼構建星辰大海

我們暫定 寬 width, 高 height, 三維空間要有深度 暫定為 depth。
對應一片閃爍的繁星, 首先要在空間裡生成一堆的星星,其次要讓星星不停的閃爍, 暫定 按線性差值變化, 有變大(三維空間就是變亮) 有變小(對應變暗) 部分關鍵程式碼如下:

var starts = [];
for (let i = 0; i < 10000; i++) {
  starts.push({
    x: Math.random() * width,
    y: Math.random() * height,
    z: Math.random() * depth,
    //當前進度 0 - 1, 當前大小按  (1 - t) * minSize + t * maxSize
    t: Math.random(),
    //變化方向
    direction: Math.random(), 
    //變化步長 暫定為常量 後面可以根據需要每個星星都不一樣
    step: 0.01,
    //最小
    minSize: 10,
    //最大
    maxSize: 20 
  });
}
animate();
function animate () {
  requestAnimationFrame(animate);
  starts.forEach((item) => {
    let { t, minSize, maxSize } = item;
    //計算當前尺寸
    item.size = (1 - t) * minSize + t * maxSize;
    //改變進度
    if (t > 1) {
      //如果已經最大了 則開始減少
      item.direction = -1; 
    } else if (t < 0) {
      //最小了 則開始變大
      item.direction = 1; 
    }
    //修改進度
    item.t += item.step * item.direction;
  });
  //根據上面的值進行渲染
  render();
}
複製程式碼

至此 我們就完成了繁星。 但是 漫天星光閃爍,是要有流星的, 這樣才夠美麗。流星就是在星空有快速劃過一條線的星星。所以可以如下:

//流星處理邏輯
var falls = [];
for (let i = 0; i < 10; i++) {
  falls.push({
    //流星的起點
    start: {
      x: Math.random() * width,
      y: Math.random() * height,
      z: Math.random() * depth
    },
    //流星的終點
    end: {
      x: Math.random() * width,
      y: Math.random() * height,
      z: Math.random() * depth
    },
    //當前進度 0 - 1, 當前位置按 二次貝塞爾曲線計算即可 
    t: Math.random(),
    step: 0.01
  });
}

function animate () {
  requestAnimationFrame(animate);
  starts.forEach((item) => {
    let { t, start as p0, end as p2} = item;
    //隨意設定一個控制點
    let p1 = {
      x: p0.x,
      y: (p0.y + p2.y) / 2,
      z: p2.z 
    };
    //按二次貝塞爾曲線 計算當前位置
    //(1 - t) * (1 - t) * p0 + 2 * t * (1 - t ) * p1  + t * t * p2
    
    let tmp = {
      x: (1 - t) * (1 - t) * p0.x + 2 * t * (1 - t ) * p1.x + t * t * p2.x,
      y: (1 - t) * (1 - t) * p0.y + 2 * t * (1 - t ) * p1.y + t * t * p2.y,
      z: (1 - t) * (1 - t) * p0.z + 2 * t * (1 - t ) * p1.z + t * t * p2.z
    };
    item.t += item.step;
    item.tmp = tmp;
    //改變進度
    if (t > 1) {
      //如果已經最大了變為0 
      item.t = 0; 
      //重置起始點
      item.start = {
        x: Math.random() * width,
        y: Math.random() * height,
        z: Math.random() * depth
      };
      item.end = {
        x: Math.random() * width,
        y: Math.random() * height,
        z: Math.random() * depth
      };
    }
  });
  //根據上面的值進行渲染
  render();
}
複製程式碼

當然 上面只是為了 講解方便, 把閃爍流星的效果分開了, 其實這兩部分是有重合的邏輯的, 在星星狀態中加一個標誌位是閃爍還是流星,然後兩個邏輯就可以合在一起了,具體見github上程式碼實現部分這裡不再囉嗦。

地球

星空有了,下面我們要構建地球了。 地球大概分為兩部分, 首先 建立一個球,在三維空間的原點,然後給地球蒙上一層紋理皮膚,就是一張平面地圖 從 -90 ~ 90, -180 -180的平面地圖 如下:

用程式碼構建星辰大海

然後就是關鍵的來了 ,地球上的交易點需要高亮, 所以也就是在相應的經緯度上 給一些高亮效果。具體定位程式碼如下:

  //經緯度轉x,y平面座標
  lnglatToXY ({lng, lat}, width, height) {
    let x = (lng - (-180)) / 360 * width;
    let y = Math.abs((lat - 90) / 180) * height;
    return {
      x,y
    };
  }
複製程式碼

然後就是生產紋理, 大背景圖片,加上熱點canvas上面按位置畫圖就可以了。

    //渲染紋理
    async function render () {
      //全球背景圖片
      let worldBg = await util.loadImg(worldSrc);
      //熱點背景圖片
      let hotBg = await util.loadImg(hotSrc);
      //畫大背景
      ctx.drawImage(worldBg, 0, 0, width, height);
      //按經緯度繪製熱點
      data.forEach((item) => {
        let lng = item.lnglat[0];
        let lat = item.lnglat[1];
        let {x, y} = util.lnglatToXY({lng, lat}, width, height);
        ctx.drawImage(hotBg, x - size / 2, y - size / 2, size, size);
      });
      //生產紋理,然後 直接對映蒙到球上就可以了
      let texture = new THREE.Texture(canvas);
      texture.needsUpdate = true;
      return texture;
    }

複製程式碼

ok,到這裡就地球就生成了。後面就讓地球每幀繞y軸旋轉既可

animate();
function animate () {
  requestAnimationFrame(animate);
  earth.rotation.y += 0.01;
  render();
}
複製程式碼

交易線

ok 旋轉的地球也有了, 下面就是要繪製 地球上的交易線了。還是首先要有經緯度 到空間座標的一個轉換。 具體原理見下面。

用程式碼構建星辰大海

  //經緯度轉空間座標的具體程式碼, r 為球體半徑
  lnglatToXYZ ({lng, lat}, r) {
    var phi = (90 - lat) * Math.PI / 180;
    var theta = -1 * lng * Math.PI / 180;
    return {
      x: r * Math.sin(phi) * Math.cos(theta),
      y: r * Math.cos(phi),
      z: r * Math.sin(phi) * Math.sin(theta)
    };
  }

複製程式碼

所以 就很明確了,垂直於地球的線, 就是 同樣的經緯度 ,不同的 半徑。所以可以按上面公式計算出 p1, p2確定出一條直線。然後和閃爍的星空道理一樣加入控制點和方向變數 大量線段就起伏變化了。關鍵程式碼如下:

//原始資料
let lines = [
  {lng, lat },
  {lng, lat },
  ....
];

//生成一些控制引數
lines.forEach((item) => {
  //控制點 0 - 1 用來計算直線的終點 t * length 這裡隨機數是為了 不同的線初始狀態不同
  item.t = Math.random(); 
  //變化方向
  item.direction = Math.random() > 0.5 ? 1 : -1;
  //變化速度 暫定都一樣
  item.step = 0.01;
  //線段的長度 這裡可以根據實際資料 比如 value來對映長度 本出為了示意 用隨機數了
  item.length =  Math.random() * r;
});

animate();
function animate () {
  requestAnimationFrame(animate);
  lines.forEach((item) => {
    let p1 = lnglatToXYZ(item.lng, item.lat, r);
    //當前的終點 用 t 
    let p2 = lnglatToXYZ(item.lng, item.lat, r + item.length * t);
    //根據 p1, p2 即可繪製一條直線
    if (item.t > 1) {
      item.direction = -1; 
    } else if (item.t < 0) {
      item.direction = 1; 
    }
    item.t += item.direction * item.step;
  });
  render();
}
複製程式碼

ok 以上就是 閃爍的繁星, 天邊劃過的流星, 旋轉的地球, 呼吸的交易線 按對應位置組合在一起, 就是星辰大海了。 當然 看到這裡 你可能會問, 星辰有了 但是 說好的大海呢, 其實有的 你仔細看 地球表面都是水啊,那就是海。

最後附上原始碼,供大家參考。https://github.com/liuwuyue/earth 具體效果見這裡 http://yiqihaiqilai.com, http://yiqihaiqilai.com?line (個人伺服器,可能會抽風, 如不能看,請自己下載下原始碼 執行下,不用來找我)

最最後的一個重要事情,我們組這種場景還有很多, 歡迎入夥,感興趣的同學可以關注專欄或者傳送簡歷至'wuyue.lwy####alibaba-inc.com'.replace('####', '@'),大家一起描繪阿里的星辰大海。~

原文地址:github.com/ProtoTeam/b…

相關文章