程式化河流後續——加入浮力系統
前言
為了給程式化河流加入浮力系統,前面搞了個浮力外掛粗略研究了一下。也寫了一篇簡要的文章總結了下:
實際測試後,發現這個外掛並不能直接用到之前河流的網格上。效果上最明顯的是這個水效果不受水面傾斜的影響,如圖。
當水往下流時,物體還在原處漂浮,顯然違反自然規律。不過這個問題很好解決:直接給物體在水流方向再加一個力水流的推力就行了
。後期還遇到了一個問題,
由於這個水系統需要漂浮物碰撞體設定為Trigger型別。所以當漂浮物碰到河岸邊界時會衝出去(物體的碰撞型別是trigger),對於這個問題,我的解決辦法是透過自己寫些方法來限制漂浮物的運動。
水平面傾斜,物體還是再原處浮動
解決問題
首先附上之前建立程式化河流的文章的連結,可以先看下這篇:
為了解決第一個問題,我首先將浮力外掛的核心功能指令碼,也就是FloatingObject。cs和WaterVolume。cs提取出來。然後對WaterVolume。cs進行大幅簡化,去掉了網格頂點分析的部分。接著開始對FloatingObject。cs的核心部分FixUpdate進行修改:
水面法線和水面高度計算的簡化。
原始外掛計算水面的法線和水面的高度使用的水面網格資料來計算的,這裡偷懶(考慮到河流網格的頂點不會太多),直接從
體元的正上方一定距離向下發射射線,得到RaycastHit
,這裡面就已經包含了碰撞點和碰撞點的法線資訊了。這樣WaterVolume。cs中的很多程式碼就都可以去掉了。
//hit中就包含了法線和碰撞點
RaycastHit
hit
=
this
。
GetHitInfoOnWater
(
worldPoint
);
float
waterLevel
=
hit
。
point
。
y
;
//從水面上方垂直向下發射射線,得到和水面的交點
public
RaycastHit
GetHitInfoOnWater
(
Vector3
worldPoint
)
{
Vector3
_originUP
=
worldPoint
+
Vector3
。
up
*
1000
;
Ray
_rayDown
=
new
Ray
(
_originUP
,
Vector3
。
down
);
RaycastHit
hit
;
if
(
Physics
。
Raycast
(
_rayDown
,
out
hit
,
Mathf
。
Infinity
,
1
<<
LayerMask
。
NameToLayer
(
“Water”
)))
{
return
hit
;
}
return
hit
;
}
2。給物體新增推力。
在建立河流的網格的時候,我們給每個河流網格都添加了一個RiverInstance。用來儲存河流的基本資訊。
==============================
RiverInstance
。
cs
==============================
public
class
RiverSegInfo
{
public
float
LengthRadio
;
public
float
halfWidth
;
public
Vector3
flowDir
;
public
Vector3
center
;
}
================================
River
。
cs
========================================
//記錄河流寬度資訊
m_riverSegmentInfos
。
Add
(
new
RiverSegInfo
()
{
LengthRadio
=
(
float
)
i
/
_resultWayPoints
。
Count
,
//當前位置所處河流長度佔河流總長度比率
halfWidth
=
_halfRiverWidth
,
//當前位置和半寬
flowDir
=
_vetexOffset
。
normalized
,
//水流流向
center
=
_resultWayPoints
[
i
]
//河流路徑中心
}
);
//儲存每條河流的必要資料
m_riverInstance
。
riverSegmentInfos
=
this
。
m_riverSegmentInfos
;
//儲存每條河流的必要資料
m_riverInstance
。
riverUVWrapAmount
=
_uvWrap
;
//河流UVY大小
m_riverInstance
。
riverLength
=
_riverLength
;
//河流總長度
m_riverInstance
。
segmentAmount
=
_resultWayPoints
。
Count
-
1
;
//河流總節點
m_riverInstance
。
riverDepth
=
riverDepth
;
//河流深度
其中就包括指定河流位置河流的流向資訊flowDir ,河流網格UV。y的大小_uvWrap。由此,透過RaycastHit中的hit。texcoord。y資訊就可以反推出體元當前所在位置的水流流向flowDir 。
==============================
RiverInstance
。
cs
======================================
//RiverInstance類方法
public
RiverSegInfo
GetFlowSegmentInfo
(
float
UVY
)
{
if
(
UVY
>
0
&&
riverSegmentInfos
。
Count
>
0
)
{
float
_lengthUVYRadio
=
UVY
/
riverUVWrapAmount
;
float
_lengthRadioPerSegment
=
1f
/
segmentAmount
;
foreach
(
var
segInfo
in
riverSegmentInfos
)
{
if
(
Mathf
。
Abs
(
segInfo
。
LengthRadio
-
_lengthUVYRadio
)
<
_lengthRadioPerSegment
*
2
)
{
// Debug。Log(segInfo。LengthRadio + “ ” + _lengthUVYRadio + “ ” + segInfo。center);
return
segInfo
;
}
}
}
return
null
;
}
================================
FloatObject
。
cs
=================================
//由hit。textureCoord。y反推水流流向flowDirection
segmentInfo
=
river
。
GetFlowSegmentInfo
(
hit
。
textureCoord
。
y
);
Vector3
flowDirection
=
segmentInfo
。
flowDir
;
有了水流流向,配合該位置水面網格的法線我們就可以計算水流推力了。這裡我使用(1。2 -Dot(N,y))作為因子來調整推力大小(水面傾斜度越大,推力越大,水完全水平時,依然會緩慢向水流方向前進),最終得到推力currentVoxelWaterForce。最後將推力與原來的浮力疊加。至此推力新增完成。物體開始沿著河流移動。
//這裡1。2f - ndotUp是確保當物體處於完全水平的水面時也能向水流方向運動
currentVoxelWaterForce
=
(
1。2f
-
ndotUp
)
*
flowDirection
*
water
。
baseWaterPushForceRadio
;
Quaternion
surfaceRotation
=
Quaternion
。
FromToRotation
(
this
。
water
。
transform
。
up
,
(
surfaceNormal
+
flowDirection
)。
normalized
);
//沉入水中越少,朝水面法線偏移越厲害,抖動的越厲害
surfaceRotation
=
Quaternion
。
Slerp
(
surfaceRotation
,
Quaternion
。
identity
,
submergedFactor
);
Vector3
finalVoxelForce
=
surfaceRotation
*
((
forceAtSingleVoxel
+
currentVoxelWaterForce
)
*
submergedFactor
);
//新增力
this
。
rigidbody
。
AddForceAtPosition
(
finalVoxelForce
,
worldPoint
);
白線即為水流推力方向
3。解決物體衝出河流的問題。
物體衝出河流的問題是必然的,因為地形碰撞體其實已經完全沒起作用了。這裡我用兩個方式來解決了一下(這兩個方式都不是精確的,只是大概達到了效果)。
首先在物體的OnTriggerEnter被觸發時,當檢測到物體是與地形網格碰撞。我將給物體在碰撞的瞬間給物體設定一個反彈速度
。
從而讓物體反彈回來並繼續往下游前進。如下圖:當前物體運動速度為V,水流推力為W,於是給物體新增的力即為VW
。
protected
virtual
void
OnTriggerEnter
(
Collider
other
)
{
//……
this
。
rigidbody
。
velocity
=
(
currentVoxelWaterForce
-
rigidbody
。
velocity
)
*
boundRadio
;
}
}
單純這樣還是不能解決問題,物體偶爾還是會鑽出去。於是後面又加了一個操作:
持續給物體新增一個拽向河流中心的力,力的大小根據物體偏離河流中心的大小來調整
。透過挑取合適的因子,終於達到了可以接受的效果。
//防止物體衝出河道
if
(
segmentInfox
!=
null
){
//與河中心的偏移值
Vector3
offset
=
transform
。
position
-
(
segmentInfox
。
center
+
river
。
riverDepth
*
Vector3
。
down
);
///將物體拉回河流中心
this
。
rigidbody
。
AddForce
(-
offset
*
centerDragForceRadio
,
ForceMode
。
Impulse
);
}
最後
透過上面的操作,河流的浮力系統終於改造完成了。最終結果:
Git:
最後貼下FloatObject。cs完整的FixUpdate程式碼,方便對照。
protected
virtual
void
FixedUpdate
()
{
if
(
this
。
water
!=
null
&&
this
。
voxels
。
Length
>
0
)
{
//將總共的浮力分到每個體元
Vector3
forceAtSingleVoxel
=
this
。
CalculateMaxBuoyancyForce
()
/
this
。
voxels
。
Length
;
Bounds
bounds
=
this
。
collider
。
bounds
;
//每個體元的高度
float
voxelHeight
=
bounds
。
size
。
y
*
this
。
normalizedVoxelSize
;
float
submergedVolume
=
0f
;
RiverSegInfo
segmentInfo
=
null
;
for
(
int
i
=
0
;
i
<
this
。
voxels
。
Length
;
i
++)
{
Vector3
worldPoint
=
this
。
transform
。
TransformPoint
(
this
。
voxels
[
i
]);
//獲取水深度
RaycastHit
hit
=
this
。
GetHitInfoOnWater
(
worldPoint
);
float
waterLevel
=
hit
。
point
。
y
;
//體元的深度(體元底部到水面)
float
deepLevel
=
waterLevel
-
worldPoint
。
y
+
(
voxelHeight
/
2f
);
// 0 - 完全出了水面 1 - 完全沉入水中
float
submergedFactor
=
Mathf
。
Clamp
(
deepLevel
/
voxelHeight
,
0f
,
1f
);
submergedVolume
+=
submergedFactor
;
//水面的法線
Vector3
surfaceNormal
=
hit
。
normal
;
//水對物體的推力計算
float
ndotUp
=
Mathf
。
Clamp01
(
Vector3
。
Dot
(
surfaceNormal
,
Vector3
。
up
));
segmentInfo
=
river
。
GetFlowSegmentInfo
(
hit
。
textureCoord
。
y
);
if
(
segmentInfo
!=
null
)
{
Vector3
flowDirection
=
segmentInfo
。
flowDir
;
//這裡1。2f - ndotUp是確保當物體處於完全水平的水面時也能向水流方向運動
currentVoxelWaterForce
=
(
1。2f
-
ndotUp
)
*
flowDirection
*
water
。
baseWaterPushForceRadio
;
Quaternion
surfaceRotation
=
Quaternion
。
FromToRotation
(
this
。
water
。
transform
。
up
,
(
surfaceNormal
+
flowDirection
)。
normalized
);
//沉入水中越少,朝水面法線偏移越厲害,抖動的越厲害
surfaceRotation
=
Quaternion
。
Slerp
(
surfaceRotation
,
Quaternion
。
identity
,
submergedFactor
);
Vector3
finalVoxelForce
=
surfaceRotation
*
((
forceAtSingleVoxel
+
currentVoxelWaterForce
)
*
submergedFactor
);
//新增力
this
。
rigidbody
。
AddForceAtPosition
(
finalVoxelForce
,
worldPoint
);
Debug
。
DrawLine
(
worldPoint
,
worldPoint
+
finalVoxelForce
。
normalized
,
Color
。
blue
);
Debug
。
DrawLine
(
worldPoint
,
worldPoint
+
flowDirection
);
}
}
submergedVolume
/=
this
。
voxels
。
Length
;
// 0 - object is fully out of the water, 1 - object is fully submerged
this
。
rigidbody
。
drag
=
Mathf
。
Lerp
(
this
。
initialDrag
,
this
。
dragInWater
,
submergedVolume
);
this
。
rigidbody
。
angularDrag
=
Mathf
。
Lerp
(
this
。
initialAngularDrag
,
this
。
angularDragInWater
,
submergedVolume
);
RaycastHit
hitx
=
this
。
GetHitInfoOnWater
(
transform
。
position
);
RiverSegInfo
segmentInfox
=
river
。
GetFlowSegmentInfo
(
hitx
。
textureCoord
。
y
);
//防止物體衝出河道
if
(
segmentInfox
!=
null
){
Vector3
offset
=
transform
。
position
-
(
segmentInfox
。
center
+
river
。
riverDepth
*
Vector3
。
down
);
this
。
rigidbody
。
AddForce
(-
offset
*
centerDragForceRadio
,
ForceMode
。
Impulse
);
Debug
。
DrawLine
(
transform
。
position
,(
segmentInfox
。
center
+
river
。
riverDepth
*
Vector3
。
down
));
}
}
}