不少人都見過風向圖,直觀形象,也是地圖數據和現實數據在可視化上很好的結合。git
這是我見的第一個風向圖,記得是2012年吧,當時以爲頗有意思,做爲一名技術人員,天然好奇它是如何作到的,是Canvas仍是SVG?但當時沒深究。最近正好有人(大哥)提到了這個,不妨深刻了解,一探究竟。因而乎,發現原來還有這麼多玩法,大同小異,好比說這個,來自earth.nullschool.net:github
固然還有來自度娘開源的echarts-x的:算法
基本上,這三個效果圖基本涵蓋了目前風向圖的技術點和功能點(我本身的見解,由於windyty是基於earth.nullschool寫的,前者多了一個worker線程處理數據,然後者在github上開源)。不知道哪個最對你的胃口?對我而言,圖1簡單易懂,能夠快速掌握風向圖的實現;圖2是實時的全球風向數據,並且是二進制格式,是大數據傳輸的一個方案;圖3則採用WebGL實時渲染,算是大數據渲染的一個方案,因此各有千秋。正好本文就結合這三個例子說一下其中處理好的地方,也是一個由易到難的過程。canvas
乍看上去,多少會以爲無從下手。這是怎麼作到的?其實吧,懂與不懂就是那一層紙,就看你願不肯意戳破而已。咱們先從數聽說起。echarts
首先介紹一下向量場(Vector Field)的概念。在維基百科的解釋是:在向量分析中,向量場是把空間中的每一點指派到一個向量的映射。物理學中的向量場有風場、引力場、電磁場、水流場等等。如圖,下面是一個二維的向量場,每個點都是一個向量。less
固然這是一個抽象的數學概念和表達,物理中的電磁場常常會用到它,在現實中其實也隨處可見,好比下面這個有意思的磁場的向量表達,密集恐懼症的人請略過~ dom
同理,風場的抽象模型也是一個向量場:每個點都有一個風速和方向,能夠分解爲在該點分別在XY方向上的向量(咱們簡化爲XY兩個方向,不考慮Z,因此惋惜不能聽《龍捲風》了),則該向量則表明該點X方向和Y方向的速度。模塊化
如上圖,是一個真實的風向圖數據。簡單來講,timestamp表明當前數據的採集時間,(x0,y0,x1,y1)分別是經緯度的範圍,而grid是該向量場的行列數,field就是向量場中每個點的速度值,若是是(0,0)則表示此點風平浪靜。可能不一樣平臺的風向圖數據有必定差異,但都大同小異。函數
向量場和數據格式,直覺上,咱們能夠知道,就是把這些向量擬合成平滑線,能夠造成以下一個真實的風向。
如何造成線,並且看上去全球範圍內有總不能只有一陣風吧(讓我想起了木星的大紅斑,這場風在木星已經吹了至少200年曆來沒停過),這揭露了兩個問題,1向量場是離散點,而線是平滑,這裏面有一個插值問題;2更麻煩的是,這些線有好多好多鏈接的方式,均可以鏈接成線,有點相似等高線的算法,怎麼連,看上去無從下手啊。
這是我看完數據後,本身以爲要實現風向效果時以爲須要解決的問題,感受好難啊。懷着這個疑問進入夢鄉,次日format了一下js腳本,本地調試後,發現個人問題是對的,但是思路是錯的。不要一上來就考慮這麼多因素,而是基於當前的狀態來解決當前的問題,就比如一道很是複雜的代數問題,或許經過幾何方式反而能夠很簡單的解決。
很少廢話了,儘管我以爲這些廢話纔是提升能力的最有價值的,解決問題不過是一個感悟過程的必然而已。好了,有了數據,看看「神諸葛」如何起風的吧。
舉個例子,給你一個圍棋棋盤(向量場),每個格子就是一個向量,你隨手拿一個棋子,隨手(隨機)放在一個格子上,這就是風的起點。下一回合(下一幀或下一秒),你根據當前格子的向量值(X值和Y值)移動棋子,就是風在當前的風速下拖着經常的尾巴跳到下一個格子上的效果。這樣,這個棋子會根據所在格子的向量值不停的移動,直到格子的向量值爲零(風停)。
也就是說只要給一個起點,我就能颳起一股風來。那給你5000個棋子(起點),你就能颳起5000股風了。固然可能兩股氣流重疊,這時可能不太符合物理規律了,由於咱們的思路下是各吹各的,不過誰關心呢。因而,基於每一幀狀態的管理,咱們能夠很簡單的模擬出風向圖的效果。很簡單巧妙吧。
好了,理論上咱們知道該怎麼作了,看看如何代碼實現。咱們也整理一下這個流程,把它們模塊化。
今天就和圍棋幹上了,仍是這個例子,首先呢就是數據,也就是棋盤和格子,也就是Vector和Vector Field這兩個對象來方便數據的讀取、管理等;其次,固然是棋子了,記錄每個棋子的生命週期,當前的位置,下一步的位置,也就是風上對應的每個幀的位置信息,這個是Particle類來記錄這些信息;最後,有了棋盤和棋子,還須要一個推手來落子,這裏稱做MotionDisplay把,負責管理每一回合(幀)下棋子對應棋盤的位置,這個類要作的事情不少:有多少個棋子、哪個還收回、須要新增幾個棋子(風粒子的管理),怎麼在棋盤上放置(渲染);等等,最後還少了一個,就是時鐘啊,每一回合但是要讀秒的哦,也就是Animation。
仍是得上代碼,否則顯得不專業。下面先把上面提到的這些對象中一些關鍵的屬性和方法說明一下,能夠知道哪些關鍵的屬性是由哪些類來管理,而一些關鍵的方法進行一個說明,你們能夠先專一類和函數自己的內容,瞭解這個拼圖的部份內容。最終會有一個初始化的函數來一個總體流程的介紹,這時你們會了解整個拼圖的面貌。
向量比較簡單,就是X和Y兩個份量,其餘的好比長度,角度這些方法就不在此贅述:
var Vector = function(x, y) { this.x = x; this.y = y; }
VectorField.read= function(data, correctForSphere) { var field = []; var w = data.gridWidth; var h = data.gridHeight; for (var x = 0; x < w; x++) { field[x] = []; for (var y = 0; y < h; y++) { var vx = data.field[i++]; var vy = data.field[i++]; var v = new Vector(vx,vy); …… field[x][y] = v; } } var result = newVectorField(field,data.x0,data.y0,data.x1,data.y1); return result; };
如此,向量場已經佈置完善,固然,對照JSON數據仔細看一下代碼,有保存了經緯度的範圍,行和列等信息,固然,該類中有其餘幾個函數沒有在此列出,好比判斷一個點是否在棋盤內,另外還有插值,由於每個網格位置都是離散的,行和列都是整數,而現實中風的走向是連續的,可能在當前時刻的位置是分數,則須要根據臨近的整數點的值插值獲取當前點的一個近似值,這裏採用的是雙線性插值,取的周圍四個點:
VectorField.prototype.bilinear= function(coord, a, b) { var na = Math.floor(a); var nb = Math.floor(b); var ma = Math.ceil(a); var mb = Math.ceil(b); var fa = a - na; var fb = b - nb; return this.field[na][nb][coord] * (1 - fa)* (1 - fb) + this.field[ma][nb][coord] * fa * (1 - fb) + this.field[na][mb][coord] * (1 - fa) * fb + this.field[ma][mb][coord] * fa * fb; };
如上是向量和向量場的一些關鍵函數和屬性。實現了讀取數據,經過getValue函數獲取任意一個位置(可使小數)的速度的X和Y份量。
下面就是棋子了,每一回合棋子的位置也就是風在每一幀的位置:
var Particle =function(x, y, age) { this.x = x; this.y = y; this.oldX = -1; this.oldY = -1; this.age = age; }
如上,XY是當前的位置,而old則是上一幀的位置,age是它的生命週期,有的時候棋子會被吃,起風了也有風停的那一刻,都是經過age來記錄它還能活多久(每一幀減一)。
如今就開始介紹這隻下棋的手了,看如何起風如何刮。
varMotionDisplay = function(canvas, imageCanvas, field, numParticles,opt_projection) { this.field = field; this.numParticles = numParticles; this.x0 = this.field.x0; this.x1 = this.field.x1; this.y0 = this.field.y0; this.y1 = this.field.y1; this.makeNewParticles(null, true); };
這是它的構造函數,用來記錄向量場的信息(範圍和速度向量),同時numParticles表示粒子數,即同時有多少條風線在地圖上顯示。projection用於經緯度和向量場之間的映射換算。最後makeNewParticles則會構建numParticles個風,並隨機賦給它們一個起點和生命週期,代碼以下:
MotionDisplay.prototype.makeNewParticles= function(animator) { this.particles = []; for (var i = 0; i < this.numParticles;i++) { this.particles.push(this.makeParticle(animator)); } }; MotionDisplay.prototype.makeParticle= function(animator) { var a = Math.random(); var b = Math.random(); var x = a * this.x0 + (1 - a) *this.x1; var y = b * this.y0 + (1 - b) * this.y1; return new Particle(x,y,1 + 40 * Math.random()); };
如上是一個簡單的建立粒子的過程:隨機在經緯度(x,y)建立一個可以存活1 + 40 *Math.random()幀的風,一共建立numParticles個這樣的隨機風。固然這裏爲了簡單示意。並無考慮隨機數是否會超出範圍等特殊狀況。
對象都構建完成了,那每一幀這隻手如何主持大局呢?兩件事情:Update和Render。
先看看如何更新:
MotionDisplay.prototype.moveThings= function(animator) { var speed = .01 * this.speedScale /animator.scale; for (var i = 0; i <this.particles.length; i++) { var p = this.particles[i]; if (p.age > 0 &&this.field.inBounds(p.x, p.y)) { var a = this.field.getValue(p.x,p.y); p.x += speed * a.x; p.y += speed * a.y; p.age--; } else { this.particles[i] = this.makeParticle(animator); } } };
如上,每一幀都根據速度*時間(幀)=距離來更新全部風粒子位置,同時檢測若是age爲負時,則從新建立一個來替換。
MotionDisplay.prototype.draw= function(animator) { var g = this.canvas.getContext('2d'); var w = this.canvas.width; var h = this.canvas.height; if (this.first) { g.fillStyle = this.background; this.first = false; } else { g.fillStyle = this.backgroundAlpha; } g.fillRect(dx, dy, w , h ); for (var i = 0; i <this.particles.length; i++) { var p = this.particles[i]; if (p.oldX != -1) { g.beginPath(); g.moveTo(proj.x, proj.y); g.lineTo(p.oldX, p.oldY); g.stroke(); } p.oldX = proj.x; p.oldY = proj.y; } };
由於代碼實在太長,給出的是關鍵步驟,先看後面的stroke過程,很明瞭,在moveThings的函數中咱們能夠獲得上一幀的位置和當前幀的風粒子的位置,在這裏鏈接起來造成了一段線。能夠想象,隨着幀數的增長,在有限的生命週期裏面,這個折線就像貪吃蛇同樣的增加:0-1-2-3-4……-n,則模擬出風的效果來下圖是第一幀和第二幀的截圖對比,仔細觀察紅線上面的那條風,這是前兩幀的長度對比,或者在看一下洛杉磯附近的風,增加的比較明顯,說明洛杉磯這幾天風比較大哦,不信去看天氣預報:
幀一
幀二
彷佛這樣就完美了,其實不是的。再一想,這條風有生命週期,到時候怎麼從地圖上把這條風擦除呢?若是不擦除豈不是就和灰同樣堆滿了,並且這個風明顯有一種漸變的效果,這是怎麼作到的?
這裏面是一個很棒的技巧,透明度backgroundAlpha,這裏採用和背景顏色同樣的RGB,但增長一個透明度爲0.02,fillRect的做用就比如每一幀都貼一層這樣的紙在上面,而後在上面畫新的,則以前的變的有點暗了,舊的愈來愈暗,達到一種逼真的效果,同時也很好的處理了新老交替。
如此,一個基本的風向圖就完成了。一樣,當你覺得一切都明瞭的時候,問題纔剛剛開始。簡單說一下下面兩個要點:實時數據和WebGL渲染。WebGL介紹有一些入門要求,可能不太容易明白,主要是氣質(思路)。
代碼讀多了,上個段子環節一下氛圍。上面例子的做者自稱藝術家,想要用新的方式來思考數據,感覺數據的美與樂趣。因而有了這個風向圖,確實是一個頗有趣的效果,但有一點不足點,做者主要是爲了尋找數據的美,並無提供一個有效的大數據實時性的方案。換句話說,這個範例仍是處於看看而已的程度。一個風向圖,你固然但願能在地圖上實時的看到具體一個區域的風向和全球的總體效果,這就須要解決數據的高效傳輸。
下面這個例子則較好的考慮了這個問題,windytv的做者是一位跳傘愛好者,每次跳傘前都要觀察天氣情況,特別是風向,因而乎就想到了這樣一個風向圖的應用。
是該網站的一個功能羅列,數據仍是很是全的,數據來源是GFS / NCEP / US National Weather Service,我發現裏面的天氣數據仍是很全,並且風向只是其中一個部分(我相信之後國外的開放大數據+HTML5下會有不少服務慢慢普及,不要錯過哦)。在程序中,風向圖的數據格式爲epak的二進制格式,也是使用ArrayBuffer的方式來傳輸和解析的,對這塊有興趣的能夠看看以前寫的《ArrayBuffer簡析》。
一種很不錯的方式就是圖片:
注意上面黑條,實際上是有八個像素的冗餘,裏面主要就是高寬,數據採集時間等信息,剩下的是一個全國範圍的360*180的風向量數據。雖然該數據也不算是實時的,但能夠實現六小時的更新,關鍵是能夠進行高效的數據傳輸解析。
另外,用圖片的好處是能夠切片,好比精度不高下能夠是全球的風向數據,精度高的時候,則能夠更新局部的切片數據,和地圖切片的思路徹底同樣,即避免插值的工做量,也能夠更清晰的顯示數據。所以,這能夠算是對第一個範例一個很好的優化。另外,仍是開源的哦,本身去找吧。
百度的風向圖雖然很耗性能,但確實技術上有不少值得學習的地方,畢竟用WebGL渲染,它是如何實現生命週期和向量場的計算,仍是有不少創新點。簡單說一下幾個關鍵處,能力有限,並且確實須要有必定的WebGL和OpenGL的瞭解,因此但願不要深究,注重別人的思路和方法便可。
先看看百度對外提供的接口使用方式:
surfaceLayers:[{
type: 'particle', distance: 3, size: [4096, 2048], particle: { vectorField: field, color: 'white', speedScaling: 1, sizeScaling: 1, number: 512 * 512, motionBlurFactor: 0.99 } }]
用法比較簡單,也是制定一個particle,裏面傳入向量場數據,number則是一幀中風的最大數,後面都是內部來控制。Echart-x的代碼稍微有點亂,最後我是用全局搜索才找到實現代碼的。
負責圖層建立和初始化的相關工做。
首先,當向量數據輸入後,生成爲一張等寬高的紋理vectorFieldTexture,每個向量(X,Y)就是該紋理上的一個點(RGBA),其中X = R, Y = G, B=0 ,A=255.。則該紋理中每個像素能夠獲取它的速度向量。
而後每一幀都會調用該圖層的UpDate來更新渲染。
update: function(deltaTime) { this._particlePass.setUniform('velocityTexture',this.vectorFieldTexture); particlePass.attachOutput(this._particleTexture1); particlePass.setUniform('particleTexture', this._particleTexture0); particlePass.setUniform('deltaTime', deltaTime); particlePass.setUniform('elapsedTime', this._elapsedTime); particlePass.render(this.renderer,frameBuffer); this._particleMesh.material.set('particleTexture',this._particleTexture1); frameBuffer.attach(this.renderer.gl, this._thisFrameTexture); frameBuffer.bind(this.renderer); this.renderer.render(this._scene,this._camera); }
可見,裏面Render了兩次,第一次是渲染到紋理(Render To Texture),其中還有一些時間參數,第二次纔是渲染到場景。
這是在更新數據,將每一點對應的速度向量和位置參數傳給shader,而真正的運算都經過Shader,直接操做顯卡來完成渲染過程。參數準備完畢,結合下面的渲染過程來具體理解。
vec4 p =texture2D(particleTexture, v_Texcoord);
if (p.w > 0.0) { vec4 vTex = texture2D(velocityTexture,p.xy); vec2 v = vTex.xy; v = (v - 0.5) * 2.0; p.z = length(v); p.xy += v * deltaTime / 50.0 *speedScaling; // Make the particle surface seamless p.xy = fract(p.xy); p.w -= deltaTime; } gl_FragColor = p;
你會看到除了語法和JS的不一樣,裏面的思路是同樣的,首先從'velocityTexture'裏面獲得xy,該紋理就是向量場中的信息,每個點則對應的是速度向量,而w則表示生命週期。通過計算後把值賦給了particleTexture
而後呢,若是你看懂了,就是如夢初醒的時候了,原來每一幀中,particleTexture裏面每個點對應了當前風的位置,在particle.fragment中更新每個點的位置,而後最終在場景中渲染出來。
voidmain()
{
vec4 p = texture2D(particleTexture,texcoord); gl_Position = worldViewProjection *vec4(p.xy * 2.0 - 1.0, 0.0, 1.0); }
一個WebGL渲染風向圖的大體思路,說的很不詳細,關鍵是思路。技術的鑽研,只要精益求精,總會有所收穫。在這個過程當中,我先想到風向圖怎麼實現的,等看明白了又想看看其餘的腳本有何不一樣處,發現了數據實時性,也看到了百度的WebGL渲染的方式,可能也會有疏漏的地方,但整體感受收穫很大,面紗揭開後,也再也不神祕。或者換句話說,風場,水流,重力場均可以按照這種方式來實現,只是計算公式上稍微調整一下就能夠。
看到這的人也不容易,但願對你也有所收穫.