unity 外部 生成 ui unity 程序化生成_unity发射斜射线


前言:

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

废话不多说,下面开始!

先来看看下面这张截图:


unity 外部 生成 ui unity 程序化生成_unity 外部 生成 ui_02


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

  1. 整体地形生成。
  2. 河流生成。

整体地形生成。

这里我没有使用Unity自带的地形,而是直接程序创建一个地形。主要也是为了熟悉其中的原理。

其实不管怎么样,地形的本质就是网格,如图。


unity 外部 生成 ui unity 程序化生成_i++_03

自己生成的地形

unity 外部 生成 ui unity 程序化生成_unity 外部 生成 ui_04

自带地形

而这些看似复杂的网格,实际都是由一些基础图元构成的。关于基础图元和普通网格的实现可以参考:

就是爱折腾:Unity基础的几何操作汇总1zhuanlan.zhihu.com

unity 外部 生成 ui unity 程序化生成_List_05

Procedral Grid, a Unity C# Tutorialcatlikecoding.com

unity 外部 生成 ui unity 程序化生成_i++_06


网格生成好后,就可以进入下一步——地形生成了。地形生成主要依赖一张高度图:通过采样高度图heightTexture,将采样的灰度值重新映射到0~maxTerrainHeight的区间。然后将这个值赋值为对应顶点位置的y值。


//采样图片颜色
float _r = heightTexture.GetPixel (_texcoordX, _texcoordY).r;
//顶点位置
//注意:需要在创建网格的时候就确定顶点的高度,否则顶点的排列可能会被优化而出现错误的顺序
vertices[i] = new Vector3 (x * offsetBetweenVert.x, maxTerrainHeight * _r, y * offsetBetweenVert.y);


这样映射完后,就完成了上面地形的效果了。

河流网格的生成

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

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


unity 外部 生成 ui unity 程序化生成_unity发射斜射线_07


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


unity 外部 生成 ui unity 程序化生成_unity 外部 生成 ui_08


根据上面示意图,我们可以通过曲线上两个相邻点计算出向量


,然后取Y轴为世界坐标系

Y轴。这样通过叉乘就可以得到河流水平面向量 V。有了向量V,就可以很简单的求出P1两边的顶点坐标了 需要注意的是,在这一步,为了让河流看起来更加自然,我们可以可它添加一定的随机性,Mathf.PerlinNoise是个不错的选择。

计算出河流的所有顶点后,我们可以按照指定的顺序去连接它们,就像之前创建地形一样。如图


unity 外部 生成 ui unity 程序化生成_List_09


上面讲了河流生成的原理,下面讲讲一些具体的实现。

首先我们需要一条曲线,我这里使用了Catmull-Rom曲线(真好用),它的好处在于曲线是会通过曲线上的每个点的(收尾两个点除外),具体细节参看这篇

就是爱折腾:贝塞尔曲线与Catmull-Rom曲线总结zhuanlan.zhihu.com

unity 外部 生成 ui unity 程序化生成_unity 外部 生成 ui_10


通过编辑器扩展的方式我可以在地形上绘制曲线:


unity 外部 生成 ui unity 程序化生成_着色器_11


然后根据曲线计算河流网格以及相关的参数


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;
    }


生成后河流网格如图:


unity 外部 生成 ui unity 程序化生成_i++_12


地形凹陷的生成

生成河流网格后我们会发现,河流基本是与地面重合的,有些地方还会凸出地面,特别不真实。


unity 外部 生成 ui unity 程序化生成_List_13


原因很简单:计算河流网格的时候,曲线本来就是和地面重合的,并且地形地面也没有凹陷,这与显示情况是完全不符合的。

我想到的方案是:将地形所有顶点抬高指定位移-》从地形每个顶点为起点,方向向下发射射线,判断是否碰撞到河流网格-》如果成功碰撞到,就将地形网格对应的顶点向下凹陷指定深度。并且为了使河床与地形过渡更自然,我通过根据碰撞点的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;

    }


unity 外部 生成 ui unity 程序化生成_unity 外部 生成 ui_14


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


unity 外部 生成 ui unity 程序化生成_unity发射斜射线_15


添加效果

最后,我写了个简单的流水的着色器,详见:

就是爱折腾:分享一个有趣的水着色器zhuanlan.zhihu.com

unity 外部 生成 ui unity 程序化生成_i++_16


然后在创建网格后,自动设置好水材质,至此这个阶段就结束了。


unity 外部 生成 ui unity 程序化生成_List_17


这个小项目可能还会有后续,因为我感觉如果能给这条河加上浮力系统就完美了。

=====================后续=============================

后续真的来了,通过一顿骚操作,终于给这条河调和加上了浮力系统。如图:


unity 外部 生成 ui unity 程序化生成_unity发射斜射线_18


简要讲解链接:

就是爱折腾:程序化河流后续——加入浮力系统zhuanlan.zhihu.com

unity 外部 生成 ui unity 程序化生成_i++_19


=====================================================

希望这篇文章对各位有所帮助,也希望大家多多支持关注我。

源代码及资源



RiverMeshDemo.unitypackage


3.9M

·

百度网盘