Cesium官方教程10--高階粒子特效
原文地址:https://cesiumjs.org/tutorials/Particle-Systems-More-Effects-Tutorial/
高階粒子系統特效
這篇教程學習更多的效果,包括天氣和火箭推進器。
如果沒有學習過粒子系統基礎知識,請學習這篇教程 粒子系統介紹 .
天氣
下雪
下雨
最開始下雪的教程是來自 追蹤聖誕老人專案裡的實現。
步驟
我們即將介紹如何做下雪效果,然後怎麼把下雪變為下雨效果。
我們將給每個粒子新增雪花圖片,然後在updateParticle函式裡定義每個粒子的移動屬性和其他動態屬性。
粒子圖片
關於表示粒子的圖片,我們可以從任一純色(紅,綠,白等)圖片開始。我們使用png格式,因為它支援透明度,所以透明的部分不可見。在本教程裡,下面三張png圖片用來建立粒子效果。最左側的圖片用來表示下雨;中間的圖片用來表示下雪;右側的圖片我們在上一教程裡已經用到了。
粒子圖片
一旦我們選定了一張圖片,我們可以在Cesium裡修改他的外觀,後面會解釋。比如,左側圓形粒子圖片將會變成前面效果圖裡長長的,藍色的更像雨滴。這個火的圖片會變成綠色的樹葉,×××的電火花,甚至白色的瀑布下面的浪花和泡沫。這裡需要創造力。
除此之外,雨雪效果裡我們設定了最開始透明度為0,最後透明度為可見性要求的值。也就是說粒子在建立後是完全不透明的。這就是為什麼在粒子的生成位置不會突然出現粒子的原因。
更新函式
使用更新函式,我們能更自由的去控制粒子的分佈、移動、以及視覺化。這裡可以簡單的修改粒子的顏色、圖片大小、生命週期等等。使用這個函式根據你需求或多或少的修改相關屬性。甚至在這個函式內部可以基於它和相機的距離修改粒子屬性(下面是示例程式碼),也可以相對某個模型或者地球去計算。
下面是我們的跟新函式程式碼:
// 下雪
var snowGravityScratch = new Cesium.Cartesian3();
var snowUpdate = function(particle, dt) {
snowGravityScratch = Cesium.Cartesian3.normalize(particle.position, snowGravityScratch);
snowGravityScratch = Cesium.Cartesian3.multiplyByScalar(snowGravityScratch,
Cesium.Math.randomBetween(-30.0, -300.0),
snowGravityScratch);
particle.velocity = Cesium.Cartesian3.add(particle.velocity, snowGravityScratch, particle.velocity);
var distance = Cesium.Cartesian3.distance(scene.camera.position, particle.position);
if (distance > (snowRadius)) {
particle.endColor.alpha = 0.0;
} else {
particle.endColor.alpha = snowSystem.endColor.alpha / (distance / snowRadius + 0.1);
}
};
第一部分程式碼,使粒子像受重力影響一樣的落下。
這個程式碼還增加一個功能,檢測粒子距離相機的距離,距離越遠,粒子越模糊(透明度越大),就像一種隨距離加重的霧效果。
其他天氣效果
除了隨著距離漸隱的粒子效果,這個示例還把 霧 和 大氣效果設定成匹配 我們正在模擬的天氣效果 。
hueShift 屬性控制了光譜顏色(the color along the color spectrum.)。saturationShift 屬性控制了實際效果的明暗分界線(how much color versus black and white the visual actually entails)。brightnessShift 屬性控制了顏色對比有多強烈(how vivid the colors are)。
霧的密度(density)屬性控制了霧顏色覆蓋在地球上有多濃厚。minimumBrightness屬性設定了霧顏色明亮度的最小值,小於這個值讓霧徹底覆蓋(acts as a way to darken the fog)。
上面的雪天,大氣顏色變得更黑,幾乎沒有顏色;霧是非常濃的白色。
最終效果
因為效果完全不同,我們建立兩個不同的粒子系統,一個模擬下雪,一個模擬下雨。
下雪和下雨
下雪
下面的程式碼使用一個基於中心位置(相機位置)的球體發射器去建立粒子系統。另外,每個粒子的圖片大小是隨機的,在給定大小和兩倍大小之間隨機,這樣粒子更加多種多樣。
這個雪的粒子系統有下面這些 和 前面我們討論過的所有屬性:
var snowParticleSize = scene.drawingBufferWidth / 100.0;
var snowRadius = 100000.0;
var snowSystem = new Cesium.ParticleSystem({
modelMatrix : new Cesium.Matrix4.fromTranslation(scene.camera.position),
minimumSpeed : -1.0,
maximumSpeed : 0.0,
lifetime : 15.0,
emitter : new Cesium.SphereEmitter(snowRadius),
startScale : 0.5,
endScale : 1.0,
image : "../../SampleData/snowflake_particle.png",
emissionRate : 7000.0,
startColor : Cesium.Color.WHITE.withAlpha(0.0),
endColor : Cesium.Color.WHITE.withAlpha(1.0),
minimumImageSize : new Cartesian2(snowParticleSize, snowParticleSize),
maximumImageSize : new Cartesian2(snowParticleSize 2.0, snowParticleSize 2.0),
updateCallback : snowUpdate
});
scene.primitives.add(snowSystem);
下雨
下雨的粒子系統和下雪的很接近,只有一點點不同:
和下雪一樣,下面的程式碼也是建立了一個基於中心位置(相機位置)的球體發射器的粒子系統。可是,我們用了不同的圖片表示雨滴, circular_particle.png,我們把它著上藍色,並垂直拉長跟想雨滴。和雪不太一樣,圖片大小不需要隨機,而是和imageSize屬性一致,這裡設定高度是寬度的2倍。
此外,下雨模擬的更新函式,有一個小小不同,雨滴的下落速度比雪花的速度快多了。下面程式碼裡我們對重力乘了一個倍率去模擬這個速度,我們也無需修改particle.velocity 而是直接修改particle.position 。
rainGravityScratch = Cesium.Cartesian3.normalize(particle.position, rainGravityScratch);
rainGravityScratch = Cesium.Cartesian3.multiplyByScalar(rainGravityScratch,
-1050.0,
rainGravityScratch);
particle.position = Cesium.Cartesian3.add(particle.position, rainGravityScratch, particle.position);
最後,確保整個環境和場景匹配,我們修改大氣和霧效果和下雨天匹配。下面程式碼修改為深藍色天空,還有一點薄霧。
如果需要了解多一些,請檢視 Sandcastle中雨雪示例.
彗星和火箭尾焰
彗星尾
火箭
使用多個粒子系統
天氣系統裡僅僅需要一個粒子系統,為了建立火箭尾焰效果,我們需要多個粒子系統。示例中每個位置的一圈粒子實際是一個完整的粒子系統。也就是說我們建立了一圈粒子系統,每個系統發射的粒子都是從噴發位置 向外發射。這就讓我們更好的控制了整體系統的移動。一個簡單的視覺化除錯手段是設定cometOptions.numberOfSystems為2,設定cometOptions.colorOptions 僅僅包含兩種顏色,效果就像下面的圖片展示的。這樣就更容易跟蹤每個系統建立的粒子的執行軌跡。
兩個粒子系統
為了系統的不同設定,我們建立了了火箭示例和彗星示例的不同配置陣列。
此外,為了便與組織程式,同時建立了兩個不同的配置物件。一個彗星版本,另一個是火箭版本。不同的初始化個數,不同的偏移位置等等配置引數導致了兩個效果的巨大差異。
var cometOptions = {
numberOfSystems : 100.0,
iterationOffset : 0.003,
cartographicStep : 0.0000001,
baseRadius : 0.0005,
colorOptions : [{
red : 0.6,
green : 0.6,
blue : 0.6,
alpha : 1.0
}, {
red : 0.6,
green : 0.6,
blue : 0.9,
alpha : 0.9
}, {
red : 0.5,
green : 0.5,
blue : 0.7,
alpha : 0.5
}]
};
var rocketOptions = {
numberOfSystems : 50.0,
iterationOffset : 0.1,
cartographicStep : 0.000001,
baseRadius : 0.0005,
colorOptions : [{
minimumRed : 1.0,
green : 0.5,
minimumBlue : 0.05,
alpha : 1.0
}, {
red : 0.9,
minimumGreen : 0.6,
minimumBlue : 0.01,
alpha : 1.0
}, {
red : 0.8,
green : 0.05,
minimumBlue : 0.09,
alpha : 1.0
}, {
minimumRed : 1,
minimumGreen : 0.05,
blue : 0.09,
alpha : 1.0
}]
};
此外,每個的colorOptions是一個數組,包含了隨機顏色,那麼效果更加隨機化。這就是說不是採用一個固定的初始化顏色,而是依據當前正在建立的粒子系統的序號來決定用哪個顏色。下面程式碼裡,i表示當前的遍歷序號。
var color = Cesium.Color.fromRandom(options.colorOptions[i % options.colorOptions.length
開始
使用下面的程式碼初始化每個系統
function createParticleSystems(options, systemsArray) {
var length = options.numberOfSystems;
for (var i = 0; i < length; ++i) {
scratchAngleForOffset = Math.PI 2.0 i / options.numberOfSystems;
scratchOffset.x += options.baseRadius Math.cos(scratchAngleForOffset);
scratchOffset.y += options.baseRadius Math.sin(scratchAngleForOffset);
var emitterModelMatrix = Cesium.Matrix4.fromTranslation(scratchOffset, matrix4Scratch);
var color = Cesium.Color.fromRandom(options.colorOptions[i % options.colorOptions.length]);
var force = forceFunction(options, i);
var item = viewer.scene.primitives.add(new Cesium.ParticleSystem({
image : getImage(),
startColor : color,
endColor : color.withAlpha(0.0),
particleLife : 3.5,
speed : 0.00005,
imageSize : new Cesium.Cartesian2(15.0, 15.0),
emissionRate : 30.0,
emitter : new Cesium.CircleEmitter(0.1),
bursts : [ ],
lifetime : 0.1,
forces : force,
modelMatrix : particlesModelMatrix,
emitterModelMatrix : emitterModelMatrix
}));
systemsArray.push(item);
}
}
下來過一遍這個粒子系統建立函式,options 表示我們要建立一個彗星尾焰或者火箭尾焰。就像前面提到得,systemsArray 儲存了依據輸入的options建立的所有粒子系統。
兩個尾焰非常相似,除了color和force之外其他的配置都相同。另外,emitterModelMatrix 也是對每個粒子系統都完全不同,在我們建立的圓環上做一個旋轉偏移,那麼它產生的粒子和前一個粒子系統產生的粒子會有一點點偏移。
我們沒有載入一個圖片檔案。而是使用 HTML canvas直接繪製了一張圖片。 儘管這裡我們只是繪製一個圓圈,但是這種方法非常有擴充套件性。比如,給 getImage增加一個當前遍歷序號的引數,依據這個引數做一個小小的改變,那就會產生不同的視覺化效果。
從零開始建立粒子圖片
既然我們已經有了思路,那就實現這個過程。不像前面直接載入圖片,我們用程式碼來建立圖片,使用程式碼還能實現更多的方法。
把粒子新增到系統裡
準備好,我們開始最關鍵的一步,讓粒子動起來。下面的程式碼是我們要在updateCallback函式內實現的:
var func = function(particle) {
scratchCartesian3 = Cesium.Cartesian3.normalize(particle.position, new Cesium.Cartesian3());
scratchCartesian3 = Cesium.Cartesian3.multiplyByScalar(scratchCartesian3, -1.0, scratchCartesian3);
particle.position = Cesium.Cartesian3.add(particle.position, scratchCartesian3, particle.position);
scratchCartographic = Cesium.Cartographic.fromCartesian(particle.position,
Cesium.Ellipsoid.WGS84,
scratchCartographic);
var angle = Cesium.Math.PI * 2.0 * iterationOffset / options.numberOfSystems;
iterationOffset += options.iterationOffset;
scratchCartographic.longitude += Math.cos(angle) * options.cartographicStep;
scratchCartographic.latitude += Math.sin(angle) * options.cartographicStep;
particle.position = Cesium.Cartographic.toCartesian(scratchCartographic);
};
但是,這是什麼?這個函式和設定到粒子系統裡的回撥函式不同。在建立粒子系統部分,我們設定 force引數用了 var force = forceFunction(options, i);。這個其實呼叫了一個輔助函式,輔助函式內部返回了實際的更新函式。
var scratchCartesian3 = new Cesium.Cartesian3();
var scratchCartographic = new Cesium.Cartographic();
var forceFunction = function(options, iteration) {
var iterationOffset = iteration;
var func = function(particle) {
scratchCartesian3 = Cesium.Cartesian3.normalize(particle.position, new Cesium.Cartesian3());
scratchCartesian3 = Cesium.Cartesian3.multiplyByScalar(scratchCartesian3, -1.0, scratchCartesian3);
particle.position = Cesium.Cartesian3.add(particle.position, scratchCartesian3, particle.position);
scratchCartographic = Cesium.Cartographic.fromCartesian(particle.position,
Cesium.Ellipsoid.WGS84,
scratchCartographic);
var angle = Cesium.Math.PI * 2.0 * iterationOffset / options.numberOfSystems;
iterationOffset += options.iterationOffset;
scratchCartographic.longitude += Math.cos(angle) * options.cartographicStep;
scratchCartographic.latitude += Math.sin(angle) * options.cartographicStep;
particle.position = Cesium.Cartographic.toCartesian(scratchCartographic);
};
return func;
};
我們這麼做有兩個原因。首先,在 JavaScript語言裡,雖然可以在for迴圈裡內建立函式,但是強烈不推薦 這麼做。其次,我們粒子更新函式需要訪問迭代器,通過它計算合適的旋轉偏移(根據angle 和iterationOffset引數計算)(這裡實際利用了js語言的閉包特性)。為了解決這些問題,我們建立一個輔助函式,在它內部返回了一個適合的更新函式。
解析這個 Force Function
updateCallback 函式和 forceFunction函式實際都幹了什麼? createParticleSystems的時候,我們沿著圓形偏移建立了每個粒子系統,同時我們也希望,當粒子從他們的初始位置移動的時候,也是也是沿著圓形偏移的方向來再偏移。
這個粒子的迭代偏移不僅僅是建立了圓形旋轉效果,而其是控制了這個圓形旋轉效果的平滑度,就像彗星和火箭的視覺化效果對比。不僅僅是依據當前角度的cosine和sin計算一個位置,我們實際上跌加了前一個位置。因此,太小的迭代迭代偏移也不足以調整這個角度,讓半徑穩定的變大才能保證系統的連續性。想法,更大的迭代偏移將會使角度更快的增加到原始位置。這麼做才能做出來很密集形的圓柱狀噴射效果。 (這塊我讀起來也很費解,我的理解大概是說,在火箭尾端構造了一圈粒子系統,但是這些粒子系統並不是向下噴射,而是說一邊向下,一邊旋轉,所以不同粒子系統裡的粒子實際在更新位置的時候也會考慮它在圓圈的位置,這種旋轉下來的效果,讓粒子看起來更密集,如果不這麼做,那麼這一圈無論增加多少粒子系統,總是會有一些縫隙,效果不好)
在這個教程裡,我們大量使用了sine和cosine函式來生成圓圈效果。可是,使用者也可以擴充套件一下,做成各種形狀,比如 Lissajous curve, Gibbs phenomenon, 甚至 square wave 。另外,使用者也可以不用三角函式,而是基於位置的噪音函式來控制粒子的位置,那樣也許更有趣。這樣將是非常有創意的。
相對定位
火箭尾焰和彗星尾
就像我們在上一篇粒子系統教程,現在我們已經做出來了效果,現在要把它合併到合適的位置,飛機的尾巴。因為我們的粒子系統是垂直的,為了得到相對於飛機的合適位置,我們需要使用 particleOffset屬性做一個細微偏移。使用particlesModelMatrix 當作每個系統的全域性位置矩陣。如同createParticleSystems 函式裡,對於每一個我們建立的粒子系統,我們使用emitterModelMatrix 來體現它在發射圓圈上的相對偏移位置。
/ 設定飛機的位置
var planePosition = Cesium.Cartesian3.fromDegrees(-75.59777, 40.03883, 800.0);
var particlesOffset = new Cesium.Cartesian3(-8.950115473940969, 34.852766731753945, -30.235411095432937);
// 設定粒子系統的相對位置
var transl = Cesium.Matrix4.fromTranslation(particlesOffset, new Cesium.Matrix4());
var translPosition = Cesium.Matrix4.fromTranslation(planePosition, new Cesium.Matrix4());
var particlesModelMatrix = Cesium.Matrix4.multiplyTransformation(translPosition, transl, new Cesium.Matrix4());
更多可以參考Sandcastle 中關於尾焰的粒子.
更多示例程式碼:
粒子系統示例
×××效果示例
天氣效果示例
尾焰效果示例