一、簡介
此次作業(yè)主要實現(xiàn)了一個利用SPH算法計算的流體,交互方式主要采用鼠標鍵盤,另利用安卓手機的加速度傳感器使用socket通信來控制,場景采用openmesh導入一個房間模型。
二、操作說明
電腦端:F1切換粒子顯示模式(用point繪制,帶紋理繪制,模型繪制),F(xiàn)2、F3控制鏡頭遠近,F(xiàn)4切換控制流體容器轉(zhuǎn)動模式(鼠標拖拽,手機加速度傳感器控制),F5切換是否讓房間跟隨容器轉(zhuǎn)動,F(xiàn)11、F12控制光照強度,wasd控制場景移動,上下左右鍵控制流體容器的移動。
安卓端:將電腦與手機連在同一wifi下,電腦端cmd使用ipconfig查詢ipv4地址,將地址輸入手機,點擊連接按鈕。(連接需在模型加載完畢后,命令行顯示port 9400 listening后連接)
三、初始思路
在剛看到題目要求的粒子系統(tǒng)時,由于沒有大的限制,一時無從下手。后來在考慮一段時間后突然想到可以實現(xiàn)一個水壺倒水,水落入水盆的場景,這樣可以使用手機的陀螺儀來控制水壺的傾角,會很有意思,尤其是如果把水盆的盆面正對屏幕,則可以實現(xiàn)adobe宣傳片中手機模仿灑水,電腦屏幕上出現(xiàn)反應的效果。
因此在考慮過后,預計實現(xiàn)需要完成三塊內(nèi)容:
1、水的粒子系統(tǒng)實現(xiàn)方式。
2、安卓手機的交互。
3、水的渲染。
在實現(xiàn)的過程中發(fā)現(xiàn),由于sph算法非常精細復雜,對于算力的要求很高,在我的電腦上,沒有加載場景模型的情況下,512個水粒子可以無卡頓,而加載后已經(jīng)出現(xiàn)卡頓,如果按照開始的設想去設計,最終效果很差,因此最后沒有實現(xiàn)倒水,而是僅僅將流體放在一個立方體容器中,進行旋轉(zhuǎn)移動控制
四、難點及攻克歷程
1、SPH算法
①在確定一開始的設想后,我首先實現(xiàn)了一個簡單的粒子系統(tǒng),接著查詢了相關(guān)論文和博客,包括動態(tài)的水面模擬[1],OpenGL中基于粒子系統(tǒng)的噴泉模擬實現(xiàn)[2]等,但這兩者的主要內(nèi)容都在于水面的模擬,而我想實現(xiàn)的是對于流體細致到每一個粒子的模擬,因此找到的幾篇文章都難以幫助我實現(xiàn)預想的效果。(最后的事實證明SPH算法過于精細,對于計算能力的要求很高,沒能實現(xiàn)預想效果,在提前答辯過程中助教指出sph算法并不是對每個粒子渲染,而是用每個粒子來計算一部分區(qū)域的流體的位置,對流體進行渲染。)
②接著去問了船建學院的同學,想知道如何計算倒水時的軌跡,一位同學給了我一個簡單的方程:
但這僅僅是計算了一個水流的直徑,模擬出來的結(jié)果應該是一個管道的形狀,顯然 不是我想要的。
另一個同學給我推薦了一個模擬軟件,我想看需要調(diào)哪些參數(shù)已得知要模擬流體需要考慮哪些因素,但這卻無法得知背后的計算模型。
③最后詢問荀琳玲助教得知了SPH算法(荀琳玲助教在作業(yè)完成過程中給了我很多幫助),于是上網(wǎng)查詢,在找了多個博客后鎖定了這個博客:SPH算法簡介[3]。
按照我的理解,SPH算法就是將水看成一個個粒子組成,粒子之間互相影響,計算每個水珠的各種屬性,根據(jù)每個水珠在一個時刻的受力計算出它下一時刻的位置,而關(guān)鍵就在于計算它的受力。
那么,粒子之間互相影響導致的受力如何計算呢?這里就需要一個“光滑核”的概念,即粒子的屬性會擴散到周圍,并且隨著距離的增加影響逐漸變小,這種隨著距離而衰減的函數(shù)被稱為“光滑核”函數(shù),最大影響半徑為“光滑核半徑”。
設想流體中某點r(此處不一定有粒子),在光滑核半徑h范圍內(nèi)有數(shù)個粒子,位置分別是,r0→,r1→,r2→,…rj→,則該處某項屬性A的累加公式為:
我們假設流體中一個位置為ri→的點,此處的密度為ρ(ri)、壓力為p(ri)、速度為u(ri),可以推導出此處的加速度a (ri)為
ri→處的密度計算公式最終為:
壓力產(chǎn)生的加速度部分:
粘度產(chǎn)生的加速度部分:
以上兩個部分的計算分別由sph_fluid_system類中的_computePressure方法和_computeForce計算。
那么現(xiàn)在的問題變成了,如何知道哪些粒子在當前粒子的光滑核半徑之內(nèi)呢?
這里采用的方法是sph_grid_container類和sph_neighbour_table類一起作用實現(xiàn)。具體的方式是,grid_container實現(xiàn)一個內(nèi)部分為一個個小方格的容器,方格是按照順序排好順序的,在particlepool中取得粒子后根據(jù)粒子位置屬性找出方格序號,進而找出方格中其它粒子,也就找到了鄰居,加入到neighbour_table,之后按照公式計算即可。
而在查詢SPH算法相關(guān)資料之前,我已經(jīng)寫了一個粒子系統(tǒng),結(jié)構(gòu)是particle類和一個particlepool類,后者負責粒子的管理。寫的時候因為沒有設想到之后會采用復雜的算法,因此直接將繪制寫為了particle類自身的一個方法。之后發(fā)現(xiàn)這樣的實現(xiàn)方式很不清晰,所以最后將繪制統(tǒng)一放到了源.cpp中,將glut與sph系統(tǒng)解耦,也就是說換一個glew或者glfw這個sph系統(tǒng)還能用。
源.cpp在每次display時sph_system調(diào)用tick方法,利用以上所述計算了粒子的加速度后計算出下一時刻粒子的位置,然后源.cpp獲取particlepool中所有粒子的位置進行繪制。
2、模型的加載
一開始僅僅在加載粒子模型時需要使用,因此使用了在作業(yè)一中寫的objloader,之后需要加載大的模型,發(fā)現(xiàn)自己寫的不夠用了。
首先想采用assimp庫加載場景模型,但網(wǎng)上大多數(shù)教程都是glew,而我用的是glut,因此最后采用OpenMesh。此處遇到一個坑,OpenMesh里應該有ws2def.h,與win2sock.h有沖突,而后者是使用安卓進行連接時用到的庫,在嘗試使用命名空間分隔無果后,發(fā)現(xiàn)include順序調(diào)換之后不再報錯,在向幾位學長請教之后發(fā)現(xiàn)命名空間解決的是命名上的沖突,其它的沖突還是得看報錯進行具體解決。
3、容器旋轉(zhuǎn),流體的重力向量保持不變。
由于容器和流體的旋轉(zhuǎn)是在渲染階段利用glrotate做的旋轉(zhuǎn),也就是說在流體看來,重力方向還是原本那樣,即在渲染的時候就跟著旋轉(zhuǎn)了,而不是在世界坐標系中的(0,-9.8,0)。(重力向量是源.cpp傳入fluid_system的參數(shù))因此需要在每次rotate之前把新的重力向量傳入fluid_system。新的重力向量是采用數(shù)學方法推出的。繞x軸轉(zhuǎn)yRotate度,繞y軸轉(zhuǎn)xRotate度,最后的位置是:
X = -9.8 * sin(xRotate / 180 * PI) * sin(yRotate / 180 * PI),
Y = -9.8 * cos(xRotate/180 * PI),
Z = 9.8 * sin(xRotate / 180 * PI) * cos(yRotate / 180 * PI)
此處遇到一個坑,math.h中采用弧度制,glrotate采用角度制,導致一段時間怎么調(diào)也不對,而我以為是公式錯了,導致在此處耽誤了很長時間,最后通過在display函數(shù)中加了一個將重力向量繪制出來查看才發(fā)現(xiàn)了這個問題并加以解決。
另外,此處的數(shù)學計算為之后做交互埋下了一個伏筆。
4、交互
首先實現(xiàn)鼠標鍵盤交互,像控制遠近,控制上下左右移動都是為了調(diào)整模型到合適的位置方便,在實現(xiàn)過程中加入的。可以通過f5切換場景模型是否跟著容器變化角度。視角的變化是采用gltranslate去變換物體位置實現(xiàn)的。
接著是實現(xiàn)安卓手機與之的交互。
首先要解決的問題自然是連接。因為以前做過一個安卓app的項目,有過cs架構(gòu)的經(jīng)驗,而那時服務器是放在阿里云上的,所以我一開始設想了這樣的架構(gòu):安卓發(fā)消息給阿里云的服務器,電腦端發(fā)消息去阿里云服務器獲取。后來才想到電腦端用的c++不一定不能寫服務器呀,于是去學習了一下socket在安卓端的java和電腦端的c++的使用,建立了連接,此時也就遇到了在上述的與openmesh沖突的問題。
控制方面,開始時設想采用陀螺儀獲取角加速度,讓流體的容器跟著旋轉(zhuǎn),可是實現(xiàn)完成后發(fā)現(xiàn)這個實現(xiàn)無法讓流體容器與手機的角度同步,很難控制。我想要的效果是,手機轉(zhuǎn)到什么位置,容器就轉(zhuǎn)到什么位置,但采用陀螺儀的話容器的旋轉(zhuǎn)就變成增量式的了。
后來查找了安卓的多個傳感器,發(fā)現(xiàn)加速度傳感器的xyz數(shù)值就是重力在三個軸上的分量,也就是是說,只要解上面那個公式的方程即可得到xRotate、yRotate的數(shù)值,這樣就可讓容器的轉(zhuǎn)動與手機同步,即
X = -9.8 * sin(xRotate / 180 * PI) * sin(yRotate / 180 * PI)
Y = -9.8 * cos(xRotate/180 * PI)
Z = 9.8 * sin(xRotate / 180 * PI) * cos(yRotate / 180 * PI).
最終實現(xiàn)成功了,不過此處的一個坑是asin、acos在遇到不合理的參數(shù)比如大于1時會報nan,出錯,導致程序掛掉,因此需要檢查一下再輸入。
但最終實現(xiàn)效果也沒有預期的好,因為涉及到了網(wǎng)絡,無法做到實時流暢的響應,還是會有延遲,考慮更流暢的方式可能是利用計算機攝像頭或者專門用于操控的設備來實現(xiàn)控制。
五、其它技術(shù)實現(xiàn)
1、場景:房間模型,可通過f4切換調(diào)整。開始時給了材質(zhì)的定義,發(fā)現(xiàn)效果很差,最后將其去掉,調(diào)整了一下光照位置讓它真實感強一些。
2、光照:通過設置一個light_intensity值作為light0的ambient,diffuse,specular的輸入,通過f11、f12增大減小light_intensity值調(diào)整光照。因為要調(diào)整,因此將光照的初始化放在display函數(shù)中,且與房間模型的相對位置不隨glrotate改變。
3、粒子系統(tǒng)物理仿真:sph算法
4、粒子系統(tǒng)模型切換:開始時采用自己寫的objloader,后來既然用了openmesh就直接也用它讀入了一個cube模型,因為計算量本來就很大,就沒有讀入更復雜的模型。
5、粒子系統(tǒng)光照、紋理映射:在texture.h中實現(xiàn)紋理的讀入,然后在粒子位置四周畫了一個立方體每一面都貼上相同的waterball.jpg這張紋理。另外,透明效果通過glBlendFunc混合顏色來實現(xiàn)。
6、交互控制:鼠標鍵盤、安卓設備。
六、總結(jié)
這次大作業(yè)雖然沒有實現(xiàn)預期那么好的效果,但是在利用安卓手機進行交互這一點還是增添了一點趣味性,并且在實現(xiàn)的過程中讓我意識到了adobe宣傳片的效果的大致實現(xiàn)思路,同時也意識到,利用網(wǎng)絡進行實時的交互這種方式不可靠,不夠快也不夠穩(wěn)定安全,我覺得這也是需要特定交互硬件的原因之一;在利用SPH算法實現(xiàn)流體的過程中,我開始時認為這個算法過于精細,一定有近似算法,在較低計算量的情況下實現(xiàn)不差于它的效果。經(jīng)過提前答辯時助教的點撥才意識到了問題所在,流體不是通過對每個粒子都進行渲染組成的,而是根據(jù)粒子的位置計算出周圍流體的位置,然后只對流體看得到的部分進行渲染。
但是由于提前答辯,有些倉促,在sph算法和交互上花了過多的時間,導致時間沒有按照打分點來分配,甚至寫著代碼跨了年,在其它方面的效果不是很好。有很多需要提升的空間,尤其是SPH算法,自己花了大量的力氣但渲染這塊沒做好導致最后的效果不好,之后會深入研究,爭取能夠?qū)崿F(xiàn)一開始的設想效果。
總的來說,此次大作業(yè)的完成過程中,學到了很多,同時也收獲了一定的成就感,后續(xù)的改善空間也很大,將來如果要實現(xiàn)adobe宣傳片中那種酷炫的效果,此次作業(yè)也能提供很多參考。
最后,感謝老師的教學和助教的指導,以及由于提前答辯獲得了很多來自老師和助教的反饋,收貨很大。
代碼量:pc端約1400? 安卓端約200
參考博客/論文:
[1]動態(tài)的水面模擬,http://blog.csdn.net/zju_fish1996/article/details/52317363
[2]OpenGL中基于粒子系統(tǒng)的噴泉模擬實現(xiàn),https://wenku.baidu.com/view/c79b56d476eeaeaad1f33068.html
[3]SPH算法簡介 https://thecodeway.com/blog/?p=83
Learnopengl? https://learnopengl-cn.github.io/intro/