Zephus 发表于 2022-11-18 09:43

Unity程序化河流生成

前言:

这几天,心血来潮,开了个小项目。没错,就是上面标题说的——程序化河流的生成。当然,这个在实际项目中可能并没有什么用,我写这个文章,主要也是想和各位分享下一些思路。希望对各位有所启发,还望大家多多支持,关注。
废话不多说,下面开始!
先来看看下面这张截图:


要实现这个效果,主要搞定两部分:

[*]整体地形生成。
[*]河流生成。
整体地形生成。

这里我没有使用Unity自带的地形,而是直接程序创建一个地形。主要也是为了熟悉其中的原理。
其实不管怎么样,地形的本质就是网格,如图。



自己生成的地形



自带地形

而这些看似复杂的网格,实际都是由一些基础图元构成的。关于基础图元和普通网格的实现可以参考:
网格生成好后,就可以进入下一步——地形生成了。地形生成主要依赖一张高度图:通过采样高度图heightTexture,将采样的灰度值重新映射到0~maxTerrainHeight的区间。然后将这个值赋值为对应顶点位置的y值。
//采样图片颜色
float _r = heightTexture.GetPixel (_texcoordX, _texcoordY).r;
//顶点位置
//注意:需要在创建网格的时候就确定顶点的高度,否则顶点的排列可能会被优化而出现错误的顺序
vertices = new Vector3 (x * offsetBetweenVert.x, maxTerrainHeight * _r, y * offsetBetweenVert.y);
这样映射完后,就完成了上面地形的效果了。
河流网格的生成

上面的步骤算是打地基。现在算是来到正题了。

[*]要生成河流,首先需要生成河流的网格。那么河流的网格该如何生成呢?来看下面的示意图


如图,我们可以把河流理解成一条网格带。网格带上的顶点对称的分布在曲线L的两边(其实不会对称,应该适当有点随机,后面会讲到)。因此,如果我们有了这条曲线L,我们就可以根据这条曲线将这条网格带的所有顶点计算出来。


根据上面示意图,我们可以通过曲线上两个相邻点计算出向量 P_{1}P_{2} ,然后取Y轴为世界坐标系Y轴。这样通过叉乘就可以得到河流水平面向量V。有了向量V,就可以很简单的求出P1两边的顶点坐标了。需要注意的是,在这一步,为了让河流看起来更加自然,我们可以可它添加一定的随机性,Mathf.PerlinNoise是个不错的选择。
计算出河流的所有顶点后,我们可以按照指定的顺序去连接它们,就像之前创建地形一样。如图


上面讲了河流生成的原理,下面讲讲一些具体的实现。
首先我们需要一条曲线,我这里使用了Catmull-Rom曲线(真好用),它的好处在于曲线是会通过曲线上的每个点的(收尾两个点除外),具体细节参看这篇
通过编辑器扩展的方式我可以在地形上绘制曲线:


然后根据曲线计算河流网格以及相关的参数
void RiverVertexCaculate (Mesh _riverMesh, ref List<Vector3> _resultWayPoints, float riverWidthExpand) {

      m_riverWidthInfos = new List<RiverWidthInfo> ();

      //是河流随机有大有小
      Vector3 _h = Vector3.zero;
      Vector3[] _vertexs = new Vector3;
      Vector2[] _uvs = new Vector2;
      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 - _resultWayPoints;
            }
            //河流水平方向
            _h = Vector3.Cross (_vetexOffset, Vector3.up).normalized;
            //河流下沉量
            Vector3 _riverSinkAmount = riverDepth * riverSickRadio * Vector3.down;
            //河流宽度
            Vector3 _wayPoint = _resultWayPoints;
            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 = RiverObject.transform.InverseTransformPoint (_resultWayPoints - _h * _halfRiverWidth + _riverSinkAmount);
            _vertexs = RiverObject.transform.InverseTransformPoint (_resultWayPoints + _h * _halfRiverWidth + _riverSinkAmount);
            //记录河流宽度信息
            m_riverWidthInfos.Add (
                new RiverWidthInfo () { LengthRadio = (float) i / _resultWayPoints.Count, halfWidth = _halfRiverWidth }
            );
            //v
            _uvs.y = _uvs.y = (float) i / (_resultWayPoints.Count - 1) * _uvWrap;
            //u
            _uvs.x = 0;
            _uvs.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);
            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 = transform.InverseTransformPoint (_worldPos - Vector3.up * riverDepth * _riverBedBlend);
            }
      }

      mesh.vertices = _vertexs;

    }


效果如下图,可以看出效果正常了:


添加效果

最后,我写了个简单的流水的着色器,详见:
然后在创建网格后,自动设置好水材质,至此这个阶段就结束了。


这个小项目可能还会有后续,因为我感觉如果能给这条河加上浮力系统就完美了。
=====================后续=============================
后续真的来了,通过一顿骚操作,终于给这条河调和加上了浮力系统。如图:


简要讲解链接:
=====================================================
希望这篇文章对各位有所帮助,也希望大家多多支持关注我。
源代码及资源
http://zhstatic.zhihu.com/assets/zhihu-components/file-icon/zhimg_answer_editor_file_other.svgRiverMeshDemo.unitypackage
3.9M
· 百度网盘

Arzie100 发表于 2022-11-18 09:48

推荐你看一下杨超复现的程序化河流,以及商店的river 资产,那个附带了一套很合适的工具哈哈哈

ChuanXin 发表于 2022-11-18 09:56

感谢推荐

stonstad 发表于 2022-11-18 09:59

大地形生成的基础视频,可以配合观看:)
https://www.bilibili.com/video/BV1nt411n7rw
https://www.bilibili.com/video/BV18t411w7J6

unityloverz 发表于 2022-11-18 10:02

牛逼啊,效果很逼真

APSchmidt 发表于 2022-11-18 10:03

请问有链接吗

zifa2003293 发表于 2022-11-18 10:07

mark

JoshWindsor 发表于 2022-11-18 10:09

这个地形生成的过于随机。平坦,没有特色。

LiteralliJeff 发表于 2022-11-18 10:14

这个是用灰度图生成的,不是随机的[思考]

xiangtingsl 发表于 2022-11-18 10:18

我说的是评论区发的b站视频里面的随机生成地形,你文章这个还可以
页: [1] 2
查看完整版本: Unity程序化河流生成