回顧上一篇第二步要求解的方程,也就是
V星是中間速度,Vn+1是下一時刻速度,rho是密度,p是壓力。V星可能有散度,而我們必須讓下一時刻速度無散度從而符合不可壓縮的性質。我們知道密度和中間速度,而壓力和下一時刻速度都是未知的。將它寫成離散化形式是
然後繼續回顧我們討論的連續方程,也就是不可壓縮流體必須要滿足散度為0的條件,不僅要在時刻n滿足,也要在時刻n+1也滿足,寫成一維形式如下:
然後令dx = 1,dt = 1,就可求得本時刻中間速度與壓力的關係
繼續變化,就能根據中間速度求得壓力
而密度需要乘的括號裡的數又是中間速度的散度。現在來看一個可以手算的簡單例子,假如求得的中間的速度u = 2x的話,那麼就希望最終的速度為 u = 0,而需要被減去的速度為u = -2x,繼而求得壓力的梯度和壓力。
我們看看一維情況下的速度分解就知道了,等式右邊那個大大的零就是我們希望求得的無散度的場的速度,也就是下一時刻的速度的微分要是0。而等式右邊第二項就是我們希望減去的那項,第二項是無旋度的,因此它可以表示為一個標量場的梯度,而壓力正好可以來充當這個標量場。
下面是一個可以手算的例子。
我們這裡避開了邊界情況,因為邊界情況介紹起來又是上千字qwq,下一篇再說如何處理邊界速度不為0的情況。
但這僅僅是一維的簡單例子,並且還事先知道了速度函數這個bug級的東西。到了二維三維,並且速度函數很複雜的時候,就沒這麼好算了。也就是,一般情況下,我們只能直接算出中間速度和中間速度的散度,而計算希望的最終的速度和壓力場就很難麻煩。
但這個解決我們在第一章就說過了,我們雖然一開始不知道確切值,但我們可以猜嘛,多猜幾次就和正確結果差不多了。所以1式右邊是壓力就是上次猜的結果,左邊的壓力就是這次的結果。
上面那個例子寫成C++代碼如下,貼到main函數裡即可運行。下面這段代碼實際上是錯的,並不能正確求得壓力,因為沒考慮邊界情況。但是看看下面的代碼至少能讓你對大體求解方法是怎樣的有個直觀的印象。
const int length = 8;double v[length], div[length], pressure[length], pressure2[length];for (int i = 0; i < length; i++){ pressure[i] = pressure2[i] = 0; if (i >= 2 && i <= 5) v[i] = 2 * i; else v[i] = 0;}for (int i = 0; i < length; i++){ printf("第%d網格速度wei%f\n", i, v[i]);}for (int j = 1; j < length - 1; j++){ div[j] = v[j + 1] - v[j]; printf("第%d個網格的散度是%f\n", j, div[j]);}for (int i = 0; i < 100; i++){ if (i % 10 == 0) printf("\n第%d次迭代\n", i); for (int j = 0; j < length; j++) { if (i % 10 == 0) printf("第%d網格壓力wei%f\n", j, pressure[j]); pressure2[j] = pressure[j]; } for (int j = 1; j < length - 1; j++) { pressure[j] = 0.5 * (pressure2[j + 1] + pressure2[j - 1] - div[j]); }}for (int i = 1; i < length - 1; i++){ double diff = pressure[i] - pressure[i - 1]; v[i] -= diff;
}for (int i = 0; i < length; i++){ printf("網格%d最終速度為%lf\n", i, v[i]);}
最後求得除了第一個網格和最後一個網格速度為0外,其它網格速度為4,雖然整個散度不為0,但現在只有邊界上有誤差,因此可以忽略掉這個誤差。以後我們使用unity模擬時將繼續忽略邊界誤差。
二維壓力求解二維形式的散度寫成微分為
不過上面這個我寫使用的中心差分(Central Difference ),也就是這個網格的下一個格子減去上一個格子再除以格子長度的兩倍。而之前使用的是前向差分(Upwind Difference),也就是這個網格減去上一個格子再除以格子長度的一倍。兩種方法各有優缺點。除此之外還有很多不同的差分法,比如Leafrog,MacCormack方法等。你可以很容易在各種計算流體力學的書上找到這些方法。
然後令dx = dy = dt = 1,求得中間速度與壓力的關係
改變一下各項位置,得出迭代需要的方程,同樣,等號左邊的p是本次迭代的結果,右邊的p是上次迭代的結果
上式密度rho所乘的括號內的式子,正好是中間速度的散度,因此將其寫為divergence,它在迭代過程中並不變化,所以在迭代之前就應該計算好防止重複計算
2式又被稱作壓力泊松方程(Pressure Possion Equation),或者說是雅可比迭代。泊松方程和第一章介紹的拉普拉斯方程的區別是,前者有源,後者無源。這裡的源就是密度乘以散度。
壓力泊松方程也可從速度分解的角度考慮,下面是一個速度分解:
無旋度場可以視為標量場的梯度,這裡的標量場就是壓力。將它們同乘以散度
那麼無散度的散度是零,因此這項刪掉。散度與梯度的聯繫請看這篇https://zhuanlan.zhihu.com/p/136836187。上面這個式子我們就建立起了壓力和速度的關係,兩個倒三角形就是拉普拉斯運算符。寫成微分形式就是
繼續寫成離散形式就是,然後加上密度項,變換一下即可得到1式
unity上的代碼實現
首先看看是附著到攝像機上的代碼,與之前c++代碼中的步驟大體相同。
Graphics.Blit(VelocityRT, VelocityRT2);AdvectionMat.SetTexture("_VelocityTex", VelocityRT2);Graphics.Blit(VelocityRT2, VelocityRT, AdvectionMat);
SplatMat.SetTexture("_VelocityTex", VelocityRT);SplatMat.SetFloat("PointerX", (float)MouseX / Screen.width);SplatMat.SetFloat("PointerY", (float)MouseY / Screen.height);SplatMat.SetFloat("PointerDX", (float)MouseDX / Screen.width);SplatMat.SetFloat("PointerDY", (float)MouseDY / Screen.height);SplatMat.SetInt("MouseDown", MouseDown);Graphics.Blit(null, VelocityRT2, SplatMat);
DivergenceMat.SetTexture("_VelocityTex", VelocityRT2);Graphics.Blit(VelocityRT2, DivergenceRT, DivergenceMat);
PressureMat.SetTexture("_DivergenceTex", DivergenceRT);for (int i = 0; i < 20; i++){ Graphics.Blit(PressureRT, PressureRT2); PressureMat.SetTexture("_PressureTex", PressureRT2); Graphics.Blit(DivergenceRT, PressureRT, PressureMat);}
SubtractMat.SetTexture("_VelocityTex", VelocityRT2);SubtractMat.SetTexture("_PressureTex", PressureRT);Graphics.Blit(VelocityRT2, VelocityRT, SubtractMat);
Graphics.Blit(DyeRT, DyeRT2);DisplayMat.SetTexture("_DyeTex", DyeRT2);DisplayMat.SetTexture("_VelocityTex", VelocityRT);Graphics.Blit(DyeRT2, DyeRT, DisplayMat);
Graphics.Blit(DyeRT, destination);
然後分別是各步的著色器代碼。第一步平流的,和本系列第二節的基本上相同,最後乘一個消散比例,讓速度不至於一直存在。
float Speed = 2.0f;float uv = i.uv - Speed * _VelocityTex_TexelSize.x * tex2D(_VelocityTex, i.uv).xy;float disspation = 0.999f;float4 col = disspation * tex2D(_VelocityTex, uv);
第二步,也就是滑鼠拖動造成的力,這裡的函數我使用的是這個webgl庫https://github.com/PavelDoGreat/WebGL-Fluid-Simulation所使用的,效果還很不錯~
float2 pointeruv = float2(PointerX, PointerY);float2 p = i.uv - pointeruv;float radius = 0.001;//圓的半徑float3 color = float3(PointerDX, PointerDY, 0.0f) * 50.0f;//根據滑鼠的方向產生不同方向的速度float3 splat = pow(2.1, -dot(p, p) / radius) * color;float3 base = tex2D(_VelocityTex, i.uv).xyz;if (MouseDown == 1)base += splat;float4 col = float4(base, 1.0f);return col;
第三步,計算散度的,方法和計算高斯模糊差不多,使用的公式為3式
float Top = tex2D(_VelocityTex, i.uv + float2(0.0f, _VelocityTex_TexelSize.y)).y;float Bottom = tex2D(_VelocityTex, i.uv + float2(0.0f, -_VelocityTex_TexelSize.y)).y;float Right = tex2D(_VelocityTex, i.uv + float2(_VelocityTex_TexelSize.x, 0.0f)).x;float Left = tex2D(_VelocityTex, i.uv + float2(-_VelocityTex_TexelSize.x, 0.0f)).x;float divergence = 0.5f * (Right - Left + Top - Bottom);return float4(divergence, 0.0f, 0.0f, 0.0f);
第四步:迭代計算壓力的,方法也和高斯模糊差不多,使用的公式為2式
float Top = tex2D(_PressureTex, i.uv + float2(0.0f, _PressureTex_TexelSize.y)).y;float Bottom = tex2D(_PressureTex, i.uv + float2(0.0f, -_PressureTex_TexelSize.y)).y;float Right = tex2D(_PressureTex, i.uv + float2(_PressureTex_TexelSize.x, 0.0f)).x;float Left = tex2D(_PressureTex, i.uv + float2(-_PressureTex_TexelSize.x, 0.0f)).x;float div = tex2D(_DivergenceTex, i.uv).x;float alpha = 0.5f;float pressure = (Top + Bottom + Right + Left - alpha * div) * 0.25f;return float4(pressure, 0.0f, 0.0f, 1.0f);
第五步,減去壓力梯度的,得到最終無散度,符合不可壓縮性質的下時刻速度,也很簡單
float Top = tex2D(_PressureTex, i.uv + float2(0.0f, _PressureTex_TexelSize.y)).y;float Bottom = tex2D(_PressureTex, i.uv + float2(0.0f, -_PressureTex_TexelSize.y)).y;float Right = tex2D(_PressureTex, i.uv + float2(_PressureTex_TexelSize.x, 0.0f)).x;float Left = tex2D(_PressureTex, i.uv + float2(-_PressureTex_TexelSize.x, 0.0f)).x;float2 velocity = tex2D(_VelocityTex, i.uv).xy;float factor = 0.5f;velocity.xy -= factor * float2(Right - Left, Top - Bottom);return float4(velocity, 0.0f, 1.0f);
第六步:用最終速度平流顏色
float2 coord = i.uv - _VelocityTex_TexelSize.x * tex2D(_VelocityTex, i.uv).xy;float4 col = tex2D(_DyeTex, coord);return col;
最後特別需要注意的是RenderTexture的格式,除了Dye以外,其它的都是R16G16B16A16_SNORM,如果不用16位那麼精度不夠,會出現各種奇怪的結果。SNORM是為了讓能RenderTexture存儲負數。
最後的坑爹的導入圖片問題,要允許讀寫,並且更改到合適的格式
完整的代碼在這個倉庫的第五次提交:
https://github.com/clatterrr/FluidSimChineseTutorialsWithUnity/tree/b66b5b15ca4ff46741c53502a0da85c33f7474ae
然後我們就可以破壞世界,扭曲現實了!【誤】
速度圖如下:
可視化我們可以分析一下剛才提到的那個webgl庫,修改最終的輸出圖像代碼如下
gl.viewport(0, 0, gl.drawingBufferWidth, gl.drawingBufferHeight);displayProgram.bind();gl.uniform1i(displayProgram.uniforms.uTexture, density.first[2]);blit(null);
中間速度可視化如下:
中間速度的散度可視化如下,滑鼠從左往右,此時在那個大紅亮圓的右側
壓力可視化如下,滑鼠從左往右,此時在那個大黑圓的右側
最後如果你搞定了本系列第四,五篇,特別是速度分解和壓力泊松方程,那麼大部分不可壓縮流體模擬代碼的主幹部分你就已經懂了,畢竟NS方程只是在歐拉方程上加個粘性項。
最後,介紹流體模擬的資料真是很稀少,因此我將本系列第四五章參考的主要資料放上來
https://developer.nvidia.com/sites/all/modules/custom/gpugems/books/GPUGems/gpugems_ch38.html首先是大名鼎鼎的GPU GEMS3,當然GPU GEM3把所有能省略的數學公式推導都省略了,直接看至少我沒看懂...不過GPU GEM3也是參考1999年的一篇論文《Stable Fluid》的http://citeseerx.ist.psu.edu/viewdoc/summary?doi=10.1.1.15.9203&rank=1,不過不建議直接看這篇論文
《Fluid Simulation for Computer Graphics》, Second Edition by Bridson, Robert ,也是很有名的一本書,前半本書基本上都在介紹模擬NS流體。這本書是2015年寫的,但這本書的前身似乎是一次notehttps://www.cs.ubc.ca/~rbridson/fluidsimulation/fluids_notes.pdf。這本書總體也很不錯,值得一讀https://b-ok.global/book/2871841/46dc7e
《Computational Fluid Dynamics Incompressible Turbulent Flows》by Takeo Kajishima Kunihiko Taira的第三章,這本書總體上也很不錯
《Essential Computational Fluid Dynamics》 by Oleg Zikanov 第10章。
《Guide To CFD》一篇很短的文章https://www.montana.edu/mowkes/research/source-codes/GuideToCFD.pdf
https://nbviewer.jupyter.org/github/barbagroup/CFDPython/blob/master/lessons/14_Step_11.ipynb就是那個NavierStokes的第11章
http://www.thevisualroom.com/02_barba_projects/barba_cfd_projects.html用python解決CFD問題的例子合集
http://folk.ntnu.no/leifh/teaching/tkt4140/._main000.html在線的有關數值分析的資料
http://jamie-wong.com/2016/08/05/webgl-fluid-simulation/,這是一個個人博客,使用Webgl模擬的,並做了簡單介紹。也省略了很多東西。這網頁經常打不開,因此我把它緩存成一個pdf了,當然圖片和文字位置有點錯亂。https://wwe.lanzous.com/iy4fthw8cva
https://wwe.lanzous.com/iy4fthw8cva,也是一個個人博客,比上面那個稍微詳細一點。同樣打開速度巨慢,因此緩存版本的PDF下載https://wwe.lanzous.com/idl0Vhw8cwb
然後是幾個github庫
https://github.com/PavelDoGreat/WebGL-Fluid-Simulation
https://github.com/kodai100/Unity_EulerianFluidSimulation,在CPU上模擬的
https://github.com/candycat1992/2DFluidSim
https://github.com/Scrawk/GPU-GEMS-2D-Fluid-Simulation
https://github.com/keijiro/StableFluids這個使用了computeShader
上一篇:光影帽子:【遊戲流體力學基礎及Unity代碼(四)】用歐拉方程模擬無粘性染料之公式推導
下一篇:光影帽子:【遊戲流體力學基礎及Unity代碼(六)】用NavierStokes方程模擬粘性染料流動
聲明:發布此文是出於傳遞更多知識以供交流學習之目的。若有來源標註錯誤或侵犯了您的合法權益,請作者持權屬證明與我們聯繫,我們將及時更正、刪除,謝謝。
作者:光影帽子
原文:https://zhuanlan.zhihu.com/p/270531017
More:【微信公眾號】 u3dnotes