前言:
这几天,心血来潮,开了个小项目。没错,就是上面标题说的——程序化河流的生成。当然,这个在实际项目中可能并没有什么用,我写这个文章,主要也是想和各位分享下一些思路。希望对各位有所启发,还望大家多多支持,关注。
废话不多说,下面开始!
先来看看下面这张截图:
要实现这个效果,主要搞定两部分:
- 整体地形生成。
- 河流生成。
整体地形生成。
这里我没有使用Unity自带的地形,而是直接程序创建一个地形。主要也是为了熟悉其中的原理。
其实不管怎么样,地形的本质就是网格,如图。
自己生成的地形
自带地形
而这些看似复杂的网格,实际都是由一些基础图元构成的。关于基础图元和普通网格的实现可以参考:
就是爱折腾:Unity基础的几何操作汇总1zhuanlan.zhihu.com
Procedral Grid, a Unity C# Tutorialcatlikecoding.com
网格生成好后,就可以进入下一步——地形生成了。地形生成主要依赖一张高度图:通过采样高度图heightTexture,将采样的灰度值重新映射到0~maxTerrainHeight的区间。然后将这个值赋值为对应顶点位置的y值。
//采样图片颜色
float _r = heightTexture.GetPixel (_texcoordX, _texcoordY).r;
//顶点位置
//注意:需要在创建网格的时候就确定顶点的高度,否则顶点的排列可能会被优化而出现错误的顺序
vertices[i] = new Vector3 (x * offsetBetweenVert.x, maxTerrainHeight * _r, y * offsetBetweenVert.y);
这样映射完后,就完成了上面地形的效果了。
河流网格的生成
上面的步骤算是打地基。现在算是来到正题了。
- 要生成河流,首先需要生成河流的网格。那么河流的网格该如何生成呢?来看下面的示意图
如图,我们可以把河流理解成一条网格带。网格带上的顶点对称的分布在曲线L的两边(其实不会对称,应该适当有点随机,后面会讲到)。因此,如果我们有了这条曲线L,我们就可以根据这条曲线将这条网格带的所有顶点计算出来。
根据上面示意图,我们可以通过曲线上两个相邻点计算出向量
,然后取Y轴为世界坐标系
Y轴。这样通过叉乘就可以得到河流水平面向量 V。有了向量V,就可以很简单的求出P1两边的顶点坐标了 。需要注意的是,在这一步,为了让河流看起来更加自然,我们可以可它添加一定的随机性,Mathf.PerlinNoise是个不错的选择。
计算出河流的所有顶点后,我们可以按照指定的顺序去连接它们,就像之前创建地形一样。如图
上面讲了河流生成的原理,下面讲讲一些具体的实现。
首先我们需要一条曲线,我这里使用了Catmull-Rom曲线(真好用),它的好处在于曲线是会通过曲线上的每个点的(收尾两个点除外),具体细节参看这篇
就是爱折腾:贝塞尔曲线与Catmull-Rom曲线总结zhuanlan.zhihu.com
通过编辑器扩展的方式我可以在地形上绘制曲线:
然后根据曲线计算河流网格以及相关的参数
void RiverVertexCaculate (Mesh _riverMesh, ref List<Vector3> _resultWayPoints, float riverWidthExpand) {
m_riverWidthInfos = new List<RiverWidthInfo> ();
//是河流随机有大有小
Vector3 _h = Vector3.zero;
Vector3[] _vertexs = new Vector3[_resultWayPoints.Count * 2];
Vector2[] _uvs = new Vector2[_resultWayPoints.Count * 2];
float _riverLength = PathHelper.PathLength (_resultWayPoints.ToArray ());
float _uvWrap = m_lastMaxRiverUVY = _riverLength / repeatLength;
for (int i = 0; i < _resultWayPoints.Count; i++) {
Vector3 _vetexOffset = Vector3.zero;
//河流流向
if (i < _resultWayPoints.Count - 1) {
_vetexOffset = _resultWayPoints[i + 1] - _resultWayPoints[i];
}
//河流水平方向
_h = Vector3.Cross (_vetexOffset, Vector3.up).normalized;
//河流下沉量
Vector3 _riverSinkAmount = riverDepth * riverSickRadio * Vector3.down;
//河流宽度
Vector3 _wayPoint = _resultWayPoints[i];
float _halfRiverWidth = (maxRiverWidth *
(1 + riverRandomWidthWholeLengthScale *
(Mathf.PerlinNoise (_wayPoint.x * riverRandomRadio.x, _wayPoint.z * riverRandomRadio.y))) +
riverWidthExpand) * 0.5f;
float _lengthPercents = (float) i / _resultWayPoints.Count;
_halfRiverWidth *= riverWidthWholeLengthCurve.Evaluate (_lengthPercents);
//计算曲线两边的顶点位置
_vertexs[2 * i] = RiverObject.transform.InverseTransformPoint (_resultWayPoints[i] - _h * _halfRiverWidth + _riverSinkAmount);
_vertexs[2 * i + 1] = RiverObject.transform.InverseTransformPoint (_resultWayPoints[i] + _h * _halfRiverWidth + _riverSinkAmount);
//记录河流宽度信息
m_riverWidthInfos.Add (
new RiverWidthInfo () { LengthRadio = (float) i / _resultWayPoints.Count, halfWidth = _halfRiverWidth }
);
//v
_uvs[2 * i].y = _uvs[2 * i + 1].y = (float) i / (_resultWayPoints.Count - 1) * _uvWrap;
//u
_uvs[2 * i].x = 0;
_uvs[2 * i + 1].x = 1;
}
_riverMesh.vertices = _vertexs;
_riverMesh.uv = _uvs;
}
生成后河流网格如图:
地形凹陷的生成
生成河流网格后我们会发现,河流基本是与地面重合的,有些地方还会凸出地面,特别不真实。
原因很简单:计算河流网格的时候,曲线本来就是和地面重合的,并且地形地面也没有凹陷,这与显示情况是完全不符合的。
我想到的方案是:将地形所有顶点抬高指定位移-》从地形每个顶点为起点,方向向下发射射线,判断是否碰撞到河流网格-》如果成功碰撞到,就将地形网格对应的顶点向下凹陷指定深度。并且为了使河床与地形过渡更自然,我通过根据碰撞点的UV和指定的AnimationCurve来微调顶点凹陷的程度。
//地形下陷
void TerrainSink (Mesh mesh) {
Vector3[] _vertexs = mesh.vertices;
for (int i = 0; i < _vertexs.Length; i++) {
//网格下沉riverDepth(这里实际是上升)->射线检测,碰撞到河,则保持网格的下沉,否则取原来网格
//的高度
Vector3 _worldPos = transform.TransformPoint (_vertexs[i]);
Vector3 _rayOriginPos = _worldPos + Vector3.up * riverDepth;
Ray _ray = new Ray (_rayOriginPos, Vector3.down);
RaycastHit hit;
if (Physics.Raycast (_ray, out hit, 100)) {
//地形按照曲线平滑下陷
float _uvxDis = Mathf.Abs (hit.textureCoord.x - 0.5f) * 2;
// float _rr = hit.textureCoord.y / m_lastMaxRiverUVY;
// float _halfWidth = GetRiverWidth(_rr);
float _riverBedBlend = riverBedCurve.Evaluate (_uvxDis);
_vertexs[i] = transform.InverseTransformPoint (_worldPos - Vector3.up * riverDepth * _riverBedBlend);
}
}
mesh.vertices = _vertexs;
}
效果如下图,可以看出效果正常了:
添加效果
最后,我写了个简单的流水的着色器,详见:
就是爱折腾:分享一个有趣的水着色器zhuanlan.zhihu.com
然后在创建网格后,自动设置好水材质,至此这个阶段就结束了。
这个小项目可能还会有后续,因为我感觉如果能给这条河加上浮力系统就完美了。
=====================后续=============================
后续真的来了,通过一顿骚操作,终于给这条河调和加上了浮力系统。如图:
简要讲解链接:
就是爱折腾:程序化河流后续——加入浮力系统zhuanlan.zhihu.com
=====================================================
希望这篇文章对各位有所帮助,也希望大家多多支持关注我。
源代码及资源
RiverMeshDemo.unitypackage
3.9M
·
百度网盘