渲染方块

在MC中,方块并不是一个3D模型,所有方块都是用4个面来构成的,一开始我选择使用Sprite Render来渲染面,但是这需要创建过多的对象,渲染会非常吃力DrawCall调用太多了,为了解决这个问题,我使用MeshRender来渲染,而且是一个MeshRender负责一个区块的渲染(1616256个方块的范围)。

具体操作是遍历需要渲染的方块信息,然后我们将能看到的面的信息添加到MeshRender中,这样可以减少渲染消耗。

代码参考:

void AddFace2Mesh(int id, Vector3 position, List<Vector3> vertices, List<int> triangles, List<Vector2> uvs, Face face, int count, Rect[] rects) {
        Vector3[] v = GetFaceVertices(face);
        Vector3 v1 = v[0] + position;
        Vector3 v2 = v[1] + position;
        Vector3 v3 = v[2] + position;
        Vector3 v4 = v[3] + position;
        vertices.Add(v1);
        vertices.Add(v2);
        vertices.Add(v3);
        vertices.Add(v4);
        triangles.Add(0 + count);
        triangles.Add(1 + count);
        triangles.Add(2 + count);

        triangles.Add(0 + count);
        triangles.Add(2 + count);
        triangles.Add(3 + count);
        //取顶面
        //var rect = rects[map.position[i, j, k]%3];

        //TODO
        id--;
        id *= 3;
        id += face switch {
            Face.Top => 0,// 如果 face 为 Top,增量为 0
            Face.Bottom => 2,// 如果 face 为 Bottom,增量为 2
            _ => 1,// 其余情况下增量为 1
        };
        var rect = rects[id];
        uvs.Add(new Vector2(rect.xMin, rect.yMin));
        uvs.Add(new Vector2(rect.xMax, rect.yMin));
        uvs.Add(new Vector2(rect.xMax, rect.yMax));
        uvs.Add(new Vector2(rect.xMin, rect.yMax));

    }

上述代码主要将面的信息添加到MeshRender中,并且指定贴图范围,为什么要指定这个呢,因为一个区块中(1616256个方块范围)有多种方块类型,每种方块的贴图是不同的,为了提高效率,我将所有方块的面加载到贴图上,然后渲染某个面的时候再读取。

代码参考:

Texture2D atlas;
    Rect[] rects;
    List<Texture2D> textures;
    void CreateAtlas() {
        //Texture2D[] textures;
        atlas = new Texture2D(2048, 2048);
        //textures = new Texture2D[2048];
        textures = new();
        textures.Add(Resources.Load<Texture2D>("Blocks/dirt"));
        textures.Add(Resources.Load<Texture2D>("Blocks/dirt"));
        textures.Add(Resources.Load<Texture2D>("Blocks/dirt"));

        textures.Add(Resources.Load<Texture2D>("Blocks/water_overlay"));
        textures.Add(Resources.Load<Texture2D>("Blocks/water_overlay"));
        textures.Add(Resources.Load<Texture2D>("Blocks/water_overlay"));

        textures.Add(Resources.Load<Texture2D>("Blocks/grass_top"));
        textures.Add(Resources.Load<Texture2D>("Blocks/grass_side"));
        textures.Add(Resources.Load<Texture2D>("Blocks/grass_side"));

        textures.Add(Resources.Load<Texture2D>("Blocks/cobblestone"));
        textures.Add(Resources.Load<Texture2D>("Blocks/cobblestone"));
        textures.Add(Resources.Load<Texture2D>("Blocks/cobblestone"));


        textures.Add(Resources.Load<Texture2D>("Blocks/snow"));
        textures.Add(Resources.Load<Texture2D>("Blocks/grass_side_snowed"));
        textures.Add(Resources.Load<Texture2D>("Blocks/dirt"));

        textures.Add(Resources.Load<Texture2D>("Blocks/ice"));
        textures.Add(Resources.Load<Texture2D>("Blocks/ice"));
        textures.Add(Resources.Load<Texture2D>("Blocks/ice"));

        AddTex("Blocks/log_oak_toplog_oak_top");
        AddTex("Blocks/log_oak");
        AddTex("Blocks/log_oak_toplog_oak_top");

        AddTex("Blocks/leaves_oak");
        AddTex("Blocks/leaves_oak");
        AddTex("Blocks/leaves_oak");


        rects = atlas.PackTextures(textures.ToArray(), 1, 2048);
        atlas.filterMode = FilterMode.Point;
    }

这部分值得注意的是添加面的顶点信息是要逆时针,同时由于透明方块的存在,透明方块应该是单独的一个MeshRenderer或者单独的材质。

地形的生成

首先我们要先考虑如何存储方块信息,我使用的是多个三维数组来储存,每个三维数组(区块)储存1625616大小的方块信息,根据玩家的位置来创建新的区块。

每个区块的生成主要是通过各种噪声图来实现,我们首先要生成高度,对于每一个x,z坐标,我们应该生成一个对应的最大高度,最大高度之上的空气(没高过海平面的则为海水),最大高度之下的为泥土,石头等,最下五层的基岩,如何生成高度呢?可以直接使用unity的柏林噪音函数来生成。

具体来说就是生成玩家会看到的区块,对于每个要生成的区块,遍历所有xz坐标获取高度,然后根据高度遍历整个高度,根据预设规则生成方块信息。

同时为了生成更加复杂多样的地形,我们可以采用多个噪音的叠加,赋予不同的频率和极值。

对于洞穴地形,我们同样可以使用噪音生成,比如三维的柏林噪音,但这样生成的噪音往往是不规则的,对于某些特殊的规则的峡谷可以通过变换频率来生成。

生物群系

MC中有多种的生物,分布在不同的环境中,如何决定一个环境主要依靠两个参数,一个是温度,另一是湿度。温度和湿度的生成同样使用噪音函数,值得注意的是温度还与高度有关。

生成建筑

小型建筑

这里举例树是如何生成的,我主要是通过噪音,温度,湿度,随机值来决定一个点(最高点)是否生成树,数的生成可以放在地形生成之后再遍历所有的点进行生成,一般来说根据温度,湿度来决定树的大小和颜色,将预设的树的信息加载到对应的地图数据中

大型建筑

这里就是村庄了,村庄的生成是否麻烦,因为不方便直接使用预设的模型数据生成,可能会与地形不适配,这里我们应该要做的是找到一块适合生成村庄的地。

首先我们先遍历所有的点,随机挑选一些作为村庄的源点,有了源点我们需要判断周围的地块是否适合生成村庄,生成多大的村庄。这里我才去了DFS来获取周边的点的信息来判断是否适合生成村庄,我们从源点出发,遍历周围的点,根据周围的点相对于源点的坡度来判断周围的点是否适合生成村庄,迭代若干次,我们就能获取到适合生成村庄的区域(这些点相对平坦)。再次基础上我们可以相当于将这些点夷为平地(这些点本身不一定是完全平坦的即高度会有偏差,我们根据源点的高度将选中相对偏差不大的点形成的区域夷为平地)在这一块地我们将我们设定好的村庄的某些建筑放入。

道路的生成可以通过A*算法来实现。

MeshRender到实体的转换

以上篇章我们主要是解决生成渲染的问题,如何交互呢?

我们给MeshRenderer的物体套上一个不规则碰撞器(直接使用Mesh)通过射线检测玩家所要交互的方块,对于处于方块交互。

对与处于交互的方块,我们根据其位置生成一个方块实体(比如说由6个Sprite Renderer组成的物体,或者单独的MeshRenderer方块),其交互方式由其脚本控制,并且删除对应的Mesh信息。

比如说,沙子会受到重力的影响,要实现这个功能我们要对每个方块的消失做监听,当方块消失后,监测该方块上方是否有沙子,如果有,则删除该沙子在Mesh中的信息(顶点,面,uv信息)然后再对应位置生成对应的单独方块实体,当沙子重新到达地面(由沙子实体挂载的脚本来监听),我们再将沙子的新信息更新到Mesh中,删除对应的实体。

效果展示

Untitled.png

Untitled.png

Untitled.png

项目链接

link_preview

文章作者: 妖白与茶色
版权声明: 本站所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 妖白与茶色的博客
喜欢就支持一下吧