3D語音天氣球(原始碼分享)——建立可旋轉的3D球
開篇廢話:
在9月份時參加了一個網站的比賽,比賽的題目是需要使用第三方平臺提供的服務做出創意的作品。
於是我選擇使用語音服務,天氣服務,Unity3D,Android來製作一個3D語音天氣預報,我給它起名叫做3D語音天氣球(好土。。。)
雖然沒獲獎但我覺得這個專案中還是有些東西比較有創意的,所以打算分享出來,或許有人會用到。
下面簡單看下效果圖:
左邊是Unity做出後在電腦上執行效果圖
右邊是Unity結合Android和語音控制之後在手機執行的效果圖(手機不會做GIF):
瞅著還不錯吧。。。
專案簡介:
專案結構:
首先這個專案的開發分為Android端和Unity3D端:
Android端:
Android端主要負責的是語音控制模組和4個按鈕,並將語音處理後的結果傳遞到Unity端中做處理
Unity3D端:
Unity端負責接收Android端語音處理後傳遞過來的資訊,和4個按鍵的反饋。
並根據不同的省市情況實時的從網上獲取天氣資訊,解析後顯示在我們製作的3D球上。
Build:
最後將Android端的程式碼以外掛的形式放入Unity端中,然後在Unity端Build成apk檔案在手機中執行。
業務簡介:
初始介面:
在初始介面最上方有個簡易的動態欄可以顯示一些提示資訊。
動態欄之下是一個省市的名稱標題和平均氣溫。
最中間的“動態球”顯示全國各省份的分佈。並且會根據該省市的平均
詳細頁面:
在選中一個省後(可以滑鼠或手勢選中,如果在手機上可以語音選中),點選詳細可以進入到詳細頁面。
在詳細頁面中顯示當前省內所有城市的天氣資訊,並且“動態球”上會顯示出天氣趨勢的小圖片。
如果選中某個城市後再次點選詳細則會在螢幕中上方顯示該城市的詳細天氣資訊,包括風速紫外線等等資訊。
功能劃分:
這個專案雖然功能比較簡單,但是程式碼也寫了不少,關鍵好多地方都是為了快速完成功能,也並沒有考慮一些框架和結構,因此邏輯看起來特別亂。
為了思路清晰,我準備分幾篇文章把這個專案詳細介紹一下。因為我覺得這樣分塊進行不僅思路清晰,還可以按需求觀看,而且也避免都寫在一篇文章中導致又臭又長邏輯混亂。
我準備分四篇文章來介紹這個專案:
今天就先來介紹一下如何建立這個可旋轉的“3D球”:
3D球的建立
下面是我將其他所有模組的程式碼都去掉後,一個純淨版的效果圖,而今天的任務就是完成它:
如圖可示,
在這個“3D”球中平均分佈這很多“小塊”。
本例中每一塊顯示的是一個標題和一行詳細資訊。
當然也可以在每塊中顯示圖片,顯示3D模型。而且還可以有更多創意,比如3D球中的每個小塊又是一個3D球,如此遞迴迴圈下去這樣是不是就是我們的宇宙了(想多了,趕緊跑回來。。。)
原理構成:
先來看下面3個圖示:
相信通過上面的圖示,大家就應該差不多知道這個3D球和球面上的"小塊"是如何構成的了。
沒錯,這個3D球是由一個大球和若干個小球組成的,小球附著在大球上,大球負責控制轉動,小球也就相應的轉動了。
再稍微詳細些:
大球是一個“透明/隱形”的一個物體,他有物理碰撞器屬性,所以可以轉動。
所有的小球均勻分佈在大球之上,小球同樣是“透明/隱形”,但小球上會附著文字和圖片等,小球永遠隨著大球轉動且文字方向永遠面向攝像機。
把上面這些弄明白之後配合Unity最簡單的幾個基礎語法和知識點就可以完成這個可動態旋轉的3D球了!
程式碼詳解:
下面的內容解需要一定Unity基礎,Unity入門很容易。我的習慣是先看最基本的視訊教程(一定要國人出的,一定要簡單無腦),之後看書看官網文件,再去搜幾個大神的blog看看。
1.建立大球
首先需要建立大球
通過:選單-GameObject-create other-sphere來建立一個球體(sphere)
調整大球的大小,位置。點掉他的Mash Renderer,這樣大球就“隱身”了。
3. 建立小球預設
小球是由程式碼動態生成的,但是我們需要先做一個小球的預設。
方法和製作大球相同,不過需要給小球增加一個Text Mesh來顯示文字,如果除了顯示文字還需要在小球身上顯示一些其他東西,可以建一個空的GameObject新增相應的元件,然後將這個GameObject加入到小球組成一個整體(prefab預設)。
這個例子中是顯示城市名和簡單的天氣資訊,城市名直接加在小球上,簡單的天氣資訊放在另一個GameObject上,調整大小後放入小球中組成一個整體。
4.動態生成小球
這個我認為是這個步驟是整個專案裡最難的地方,他的難度在於要根據小球數量計算出分佈在大球身上的座標。
簡單來說就是在球體表面平均分割點,並求出每個點的三維座標。
如果我能算出來就是大神了。。。還好有Google,我在Google中搜到了一個演算法如下:
float inc = Mathf.PI * (3 - Mathf.Sqrt (5));
float off = 2 / N;
for (int k = 0; k < (N); k++) {
float y = k * off - 1 + (off / 2);
float r = Mathf.Sqrt (1 - y * y);
float phi = k * inc;
Vector3 pos = new Vector3 ((Mathf.Cos (phi) * r * size), y * size, Mathf.Sin (phi) * r * size);
}
N為需要平均分為多少份。size為分割小球的大小,r為大球半徑。
for迴圈中的Vector3型別的pos就是計算所得的三維座標。我也不知道這個演算法是什麼原理,數學好的希望給講解一下。
更多的在球體上分割點的演算法可以在Google上試著搜下關鍵詞:distributed points on sphere
通過演算法求出每個點的座標後,通過下面程式碼就可以創建出小球:
GameObject text = (GameObject)Instantiate (textObject, pos, Quaternion.identity);
textObject是我們之前做的小球預設, pos是剛剛求出的小球座標。最後迴圈建立所有小球的效果如下:
5. 設定小球文字和顏色
上面建立好名為text的GameObject物件就是我們的小球。下面就要為每個小球設定父親,顏色,大小,中心文字,詳細資訊。
// 將大球設定為小球的父親(將小球放在大球身上)
text.transform.parent = gameObject.transform;
// 獲取小球的TextMesh元件
TextMesh tm = (TextMesh)text.GetComponent<TextMesh> ();
// 設定文字
tm.text = city.getName ();
// 設定顏色
tm.color = city.getWeatherColor ();
// 設定大小
text.transform.localScale = city.getSize ();
// 迴圈獲取小球的孩子(也就是小球裡面新增的GameObject,這裡只有一個顯示詳細資訊)
foreach (Transform child in text.transform) {
TextMesh tm2 = (TextMesh)child.GetComponent<TextMesh> ();
tm2.text = city.getTempture ();
tm2.color = city.getWeatherColor ();
}
6. 設定小球朝向,透明度,顏色
因為整個大球是不停在旋轉,所以大球上的小球們的朝向,透明度,和顏色都會不停的變化。所以需要實時的調整小球的各種屬性。
在update()函式中:
// 遍歷大球中的小球,設定小球的朝向,透明度,顏色
foreach (Transform child in gameObject.transform) {
// 小球永遠朝向攝像機
child.LookAt (new Vector3 (cameraTarget.position.x, cameraTarget.position.y, -cameraTarget.position.z));
// 通過距離設定小球的透明度,產生越遠越透明的效果
float dis = Vector3.Distance (child.position, cameraTarget.transform.position);
Color c = child.renderer.material.color;
float alpha = Mathf.Abs (dis - r) / 5f + 0.1f;
// 設定小球和它子gameobject的顏色
child.renderer.material.color = new Color (c.r, c.g, c.b, alpha);
foreach (Transform cc in child.transform) {
cc.renderer.material.color = new Color (c.r, c.g, c.b, alpha);
}
}
3D球的旋轉:
上面我們已經基本設定後大球和其身上的小球們(現在他們是一個整體)。
下面我們就需要讓他們旋轉起來,這時我們需要為大球新增碰撞器,這樣我們才能點選它並旋轉它。
Component-Physics-Sphere Collider
通過下面程式碼可以進行手勢/滑鼠旋轉(如果快速轉動會有一個逐漸變慢到最終停止的效果)
初始化:
//是否被拖拽//
private bool onDrag = false;
//旋轉速度//
private float speed = 3f;
//阻尼速度//
private float tempSpeed;
//滑鼠沿水平方向移動的增量//
private float axisX;
//滑鼠沿豎直方向移動的增量//
private float axisY;
//滑動距離(滑鼠)
private float cXY;
重寫滑鼠按下和拖拽函式:
//滑鼠移動的距離
void OnMouseDown ()
{
//接受滑鼠按下的事件//
axisX = 0f;
axisY = 0f;
}
//滑鼠拖拽時的操作
void OnMouseDrag ()
{
onDrag = true;
axisX = -Input.GetAxis ("Mouse X");
axisY = Input.GetAxis ("Mouse Y");
cXY = Mathf.Sqrt (axisX * axisX + axisY * axisY);
//計算滑鼠移動的長度//
if (cXY == 0f) {
cXY = 1f;
}
}
計算阻尼(為了實現上面提到的越轉越慢效果)
//計算阻尼速度
float Rigid ()
{
if (onDrag) {
tempSpeed = speed;
} else {
if (tempSpeed > 0) {
//通過除以滑鼠移動長度實現拖拽越長速度減緩越慢
if (cXY != 0) {
tempSpeed -= speed * 2 * Time.deltaTime / cXY;
}
} else {
tempSpeed = 0;
}
}
return tempSpeed;
}
最後在Update函式中判斷: // 根據計算出的阻尼和X,Y軸的偏移來旋轉大球
gameObject.transform.Rotate (new Vector3 (axisY, axisX, 0) * Rigid (), Space.World);
// 如果滑鼠離開螢幕則標記為已經不再拖拽
if (!Input.GetMouseButton (0)) {
onDrag = false;
}
寫在最後:
上面所有的程式碼都只是一個思路和一些必要的步驟。如果大家有興趣,可以下載它的原始碼來看看。