前言
为了给程序化河流加入浮力系统,前面搞了个浮力插件粗略研究了一下。也写了一篇简要的文章总结了下:
就是爱折腾:NaughtyWaterBuoyancy浮力插件解析zhuanlan.zhihu.com实际测试后,发现这个插件并不能直接用到之前河流的网格上。效果上最明显的是这个水效果不受水面倾斜的影响,如图。当水往下流时,物体还在原处漂浮,显然违反自然规律。不过这个问题很好解决:直接给物体在水流方向再加一个力水流的推力就行了。后期还遇到了一个问题,由于这个水系统需要漂浮物碰撞体设置为Trigger类型。所以当漂浮物碰到河岸边界时会冲出去(物体的碰撞类型是trigger),对于这个问题,我的解决办法是通过自己写些方法来限制漂浮物的运动。
解决问题
首先附上之前创建程序化河流的文章的链接,可以先看下这篇:
就是爱折腾:Unity程序化河流生成zhuanlan.zhihu.com为了解决第一个问题,我首先将浮力插件的核心功能脚本,也就是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:
https://gitee.com/jiuyueqiji123/NextGENProject/tree/master/NextGENProject/Assets/Samples/%E6%B2%B3%E6%B5%81%E6%B0%B4%E6%95%88%E6%9E%9Cgitee.com最后贴下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));
}
}
}