向往夏威夷清澈湛藍(lán)的海水散罕,干凈柔軟的沙灘赃泡,可惜疫情讓我們無(wú)法遠(yuǎn)足.
只要心中有沙,哪里都是馬爾代夫∩猛快來(lái)動(dòng)森小憩一會(huì)吧
老大:
嗯,這個(gè)沙灘不錯(cuò),蜿蜒曲折,煞是好看呀狼,就是它了,看看怎么實(shí)現(xiàn)损离。
碼農(nóng):
這個(gè)簡(jiǎn)單哥艇,讓美術(shù)做個(gè)模型放上去就行了。僻澎。貌踏。
老大:
有你沒(méi)你都一樣,你準(zhǔn)備去度個(gè)長(zhǎng)假嗎窟勃?
碼農(nóng):
不祖乳,不,我馬上就去想辦法秉氧!
仔細(xì)觀察眷昆,沙灘應(yīng)該是一個(gè)面片,控制其中一部分頂點(diǎn)沉入水平面以下汁咏,就出現(xiàn)了水沙交界線亚斋,蜿蜒曲折嗎,有幾種實(shí)現(xiàn)方式攘滩,可以給地編提供個(gè)頂點(diǎn)筆刷帅刊,還可以畫(huà)個(gè)曲線出來(lái),用來(lái)標(biāo)記交界線漂问。筆刷的方式比較簡(jiǎn)單赖瞒,但是數(shù)據(jù)存儲(chǔ)量較大,要把頂點(diǎn)坐標(biāo)都記錄下來(lái)以便還原蚤假,我們就采用第二種方式來(lái)實(shí)現(xiàn)栏饮。既然是曲線,非Beizer莫屬勤哗。
首先給場(chǎng)景中加一個(gè)Bezier功能抡爹,這個(gè)就不細(xì)說(shuō)了,網(wǎng)上資源一大把芒划。直接上效果
現(xiàn)在交界線有了,下面就是怎么做出沙灘的斜坡了欧穴,首先做一個(gè)面片做地形民逼,通過(guò)編輯面片的頂點(diǎn)Y坐標(biāo),產(chǎn)生斜坡:
Bezier曲線的特性是可以推進(jìn)time,算出曲線上的點(diǎn)涮帘,這樣我們就可以細(xì)分曲線拼苍,算出很多的采樣點(diǎn),連接相鄰的采樣點(diǎn),在線段右邊的頂點(diǎn)向下做個(gè)偏移疮鲫,根據(jù)離線段的距離算出偏移量
上圖中最大的長(zhǎng)方形是地形的面片吆你,曲線是畫(huà)出來(lái)的Bezier海岸線,左下是局部放大俊犯,其中A,B,C,D是曲線上的幾個(gè)采樣點(diǎn)妇多,用直線連接相鄰采樣點(diǎn),形成線段AB,BC,CD. 圖中小圓圈代表地形面片的三角形頂點(diǎn)燕侠。下面面臨的問(wèn)題就是
- 怎么確定每個(gè)頂點(diǎn)要和哪段線段進(jìn)行計(jì)算
- 怎么算頂點(diǎn)在線段的左右邊
- 頂點(diǎn)離線段的長(zhǎng)度,比如 |EF|者祖。
在分析上面三個(gè)問(wèn)題之前,擺在我們面前的是如何遍歷采樣點(diǎn)形成的線段和地形的所有頂點(diǎn)的問(wèn)題绢彤。最直觀的方式是第一層循環(huán)遍歷采樣線段(相鄰采樣點(diǎn)連線七问,下同),第二層循環(huán)遍歷地形所有頂點(diǎn)茫舶。這里我們遇到的問(wèn)題是:
- 為了讓海岸線看起來(lái)弧度自然械巡,我們細(xì)分Bezier曲線做的比較密,同樣為了彎曲時(shí)不出現(xiàn)棱角饶氏,地形面片網(wǎng)格也很密坟比,這意味著線段多,頂點(diǎn)多嚷往,計(jì)算量是二者的乘積葛账。
- 另一個(gè)是我們不好判斷哪個(gè)頂點(diǎn)該和哪個(gè)線段做計(jì)算的問(wèn)題。
左邊右邊可以通過(guò)向量點(diǎn)擊確定點(diǎn)到線段的距離也有現(xiàn)成的公式皮仁。
現(xiàn)在不好解決的就是計(jì)算量大籍琳,效率低以及不好確定頂點(diǎn)屬于哪個(gè)采樣線段。有沒(méi)有更好的解決方案呢贷祈?答案是肯定的趋急。
我們放棄采用采樣線段的方案,改用角度細(xì)分势誊,我們先確定Bezier曲線包圍盒的中心點(diǎn)呜达,然后把中心點(diǎn)和每個(gè)采樣點(diǎn)做連線。這樣每相鄰的兩個(gè)采樣點(diǎn)和中心連線粟耻,就會(huì)形成一個(gè)夾角查近,落在這個(gè)夾角所覆蓋的扇形區(qū)域內(nèi)的所有頂點(diǎn),我們就和這兩個(gè)(后面說(shuō)為什么是兩個(gè))采樣點(diǎn)做計(jì)算挤忙。為了便于理解霜威,上圖:
圖中多了一個(gè)Bezier曲線包圍盒以及中心點(diǎn),中心點(diǎn)和采樣點(diǎn)的連線(只畫(huà)出了部分連線册烈,360度都會(huì)被角度細(xì)分).下面給出包圍盒以及中心點(diǎn)的算法:
int segmentCount = bezier.Segments * bezier.CurveCount;
//計(jì)算包圍盒
for (int i = 0; i < segmentCount ; i++)
{
float time = (float)i / (segmentCount - 1);
Vector3 point = bezier.GetPoint(time);
checkPointList.Add(point); //檢查點(diǎn)收集到一個(gè)List中戈泼,以便后面使用
if (i == 0)
{
boundingBox = new Vector4(point.x, point.y, point.x, point.y);
}
else
{
if (point.x - boundingBox.x < 0)
{
boundingBox.x = point.x;
}
if (point.z - boundingBox.y < 0)
{
boundingBox.y = point.z;
}
if (point.x > boundingBox.z)
{
boundingBox.z = point.x;
}
if (point.z > boundingBox.w)
{
boundingBox.w = point.z;
}
}
}
//取中心點(diǎn)
centerPoint.Set(boundingBox.x + (boundingBox.z - boundingBox.x) / 2f, 0, boundingBox.y + (boundingBox.w - boundingBox.y) / 2f);
然后再遍歷一遍,把所有連線的角度記錄下來(lái):
//角度值的容器
checkAngleArray = new float[checkPointList.Count];
//把中心點(diǎn)和每個(gè)檢查點(diǎn)的連線,相對(duì)于X軸正方向的夾角存下來(lái)
int index = 0;
foreach (Vector3 point in checkPoints)
{
Vector3 relativePoint = point - centerPoint;
float angle = Vector3.Angle(Vector3.right, relativePoint);
if (relativePoint.z < 0)
{
angle = -angle;
}
checkAngleArray [index++] = angle;
}
下面就該遍歷每個(gè)頂點(diǎn)了大猛,一層循環(huán)大大減少了計(jì)算量:
Mesh mesh = obj.GetComponent<MeshFilter>().mesh;
Vector3[] vertices = mesh.vertices;
//中心點(diǎn)坐標(biāo)轉(zhuǎn)換到本地坐標(biāo)
Vector3 localCenterPoint = obj.transform.InverseTransformPoint(centerPoint);
//遍歷頂點(diǎn)
for (int i = 0; i < vertices.Length; i++)
{
Vector3 vertex = vertices[i];
//To do process vertex
}
框架已經(jīng)搭起來(lái)了扭倾,下面就是每個(gè)頂點(diǎn)具體怎么計(jì)算的問(wèn)題了,我們當(dāng)然可以用上面提到的向量點(diǎn)乘確定左右邊挽绩,頂點(diǎn)到線段的距離確定Y值偏移量膛壹,但是這樣的計(jì)算量還是比較大的,我們可以采用近似算法:
我們把頂點(diǎn)E也和中心點(diǎn)(標(biāo)記為O,下同) 連接起來(lái)形成 EO琼牧,那我們只要計(jì)算 |EO| - |BO|就可以了恢筝,如果大于0則在右側(cè),小于零在左側(cè)巨坊,得到的那瞬郏可以確定Y值向下的偏移量。一次計(jì)算解決了兩個(gè)問(wèn)題趾撵,效率飆升侄柔,雖然結(jié)果不是很準(zhǔn)確,但是還有補(bǔ)救辦法占调,就是我們?cè)俸虲做一次計(jì)算 |EO| - |CO|,同樣得到一個(gè)Y值偏移量暂题,然后根據(jù)角BOE/BOC進(jìn)行一個(gè)差值。確定最終Y偏移值.(這就是前面為什么說(shuō)要和兩個(gè)檢查點(diǎn)做計(jì)算的原因)
下面給出偽代碼:
//計(jì)算頂點(diǎn)到中心點(diǎn)的夾角
Vector3 vertextLine = vertex - localCenterPoint; //頂點(diǎn)到中心點(diǎn)的連線
float angle = Vector3.Angle(Vector3.right, vertextLine); //頂點(diǎn)到中心點(diǎn)的角度
//獲取檢查點(diǎn)索引
int index = GetCheckPointIndex(angle);
//頂點(diǎn)到中心點(diǎn)的距離
float vertexDistance = Vector3.Distance(vertex, localCenterPoint);
//檢查點(diǎn)到中心的距離
float checkDistance = Vector3.Distance(obj.transform.InverseTransformPoint(checkPoints[index]), localCenterPoint);
float distance = vertexDistance - checkDistance;
if(distance > 0)
{
//計(jì)算頂點(diǎn)的高度偏移值
float delta = distance * distance * heightScale;
vertex.y -= delta;
}
上面的屬于偽代碼究珊,大家在理解程序意圖的前提下很容易完善薪者,這里主要是沒(méi)有加入和第二個(gè)檢查點(diǎn)的計(jì)算以及最終差值的部分。
另外這里有個(gè)函數(shù)是GetCheckPointIndex(angle)剿涮,這個(gè)是根據(jù)頂點(diǎn)和中心點(diǎn)連線EO的角度言津,快速索引到落在那個(gè)細(xì)分角度覆蓋范圍,這里面也有效率優(yōu)化取试,首先根據(jù)角度悬槽,除以每個(gè)檢查點(diǎn)覆蓋的平均角度(360/檢查點(diǎn)個(gè)數(shù)),大體算出第幾個(gè)檢查點(diǎn)瞬浓,然后從這個(gè)檢查點(diǎn)再向前或者向后遍歷尋找初婆,這樣大大提高了效率.代碼就不貼了,請(qǐng)自行完善猿棉。
到這里磅叛,核心算法基本就完成了,看看最終效果吧:
畫(huà)面糙了些铺根,湊合看宪躯,核心是算法思想,以及解決問(wèn)題的思路位迂,解決過(guò)程。希望能夠幫到有需要各位碼農(nóng)。
碼農(nóng):
老大掂林,快來(lái)呀臣缀,沙灘做好了,就等你的北冰洋汽水了泻帮。精置。。