Can this code that generates ground tiles be optimized at all?

It can indeed seem a bit overwhelming! The most important thing for now is to just gain a good understanding of how the basic geometry structures work. Once you get to grips with all that, what I wrote in that manual page should be easier to absorb.

That’s actually pretty much what it means, yes. Each “spot” is a component of a certain numeric type (e.g. int or float) within a column that represents a specific piece of vertex data (position, normal, etc.).
A GeomVertexFormat consists of one or multiple GeomVertexArrayFormats, which in turn consist of one or more GeomVertexColumns.

Well you’re not all that far off; the intent would be to add all of the tiles to one big mesh–or at least a reasonable minimal amount of “chunks”, to allow more efficient culling of those chunks that are not in view.
These chunks could then be parented to your ground_node.

To start, an empty mesh can be created for such a chunk:

    def create_chunk(self, chunk_id, parent):

        # create an initially empty "chunk" model parented to the ground node
        vertex_format = GeomVertexFormat.get_v3n3t2()
        vertex_data = GeomVertexData("chunk_data", vertex_format, GeomEnums.UH_dynamic)
        tris_prim = GeomTriangles(GeomEnums.UH_dynamic)
        # allow adding more than 65536 vertices
        tris_prim.set_index_type(GeomEnums.NT_uint32)
        chunk_geom = Geom(vertex_data)
        chunk_geom.add_primitive(tris_prim)
        chunk_node = GeomNode(chunk_id)
        chunk_node.add_geom(chunk_geom)
        chunk = parent.attach_new_node(chunk_node)

The vertex format used in the code above doesn’t accommodate vertex colors; these can be added if you really need them, although you’re probably just going to apply textures to the chunks, I presume?
Speaking of textures, when you add tiles to one single mesh, they will all have to make use of one single texture as well, a so-called texture atlas, which combines all of the textures you created for the different tile types. If those separate textures aren’t too high-res, it shouldn’t be a problem to combine them into one big atlas. One more thing will need to be done as a result of switching to such a texture atlas: the UVs (texture coordinates) of each tile model will have to be adjusted so the model displays the correct part of the atlas. In Blender it’s possible to select multiple models together and edit their UVs simultaneously I believe, so it will hopefully not be too cumbersome to do this.

Now you can load and add the tile models to the initially empty chunk mesh:

    def load_tile(self, model_path):

        # load tile model;
        # the actual model is a child of a `ModelRoot` node
        tile = self.loader.load_model(model_path).children[0]
        tile_geom = tile.node().modify_geom(0)
        vertex_data = tile_geom.modify_vertex_data()
        # make the format of the loaded model compatible with the chunk model
        vertex_data.format = vertex_format
        # keep a memoryview of the tile vertex data around, so it can be
        # assigned to a subview of the chunk vertex data at any time
        tile_data_view = memoryview(vertex_data.arrays[0]).cast("B").cast("f")
        tile_prim = tile_geom.modify_primitive(0)
        tile_prim = GeomTriangles(tile_prim.decompose())
        tile_prim.set_index_type(GeomEnums.NT_uint32)
        # keep a memoryview of the tile primitive around, so it can be
        # assigned to a subview of the chunk primitive at any time
        tile_prim_view = memoryview(tile_prim.get_vertices()).cast("B").cast("I")

        # the number of vertices in each tile
        tile_data_size = vertex_data.get_num_rows()

        # the number of vertex indices in the GeomTriangles primitive of each tile
        tile_prim_size = tile_prim.get_num_vertices()

        return tile_data_view, tile_prim_view, tile_data_size, tile_prim_size

    def add_tile(self, tile_data, chunk, pos):

        chunk_geom = chunk.node().modify_geom(0)
        chunk_data = chunk_geom.modify_vertex_data()
        chunk_array = chunk_data.modify_array(0)
        chunk_data_size = chunk_array.get_num_rows()
        chunk_array.set_num_rows(chunk_data_size + tile_data["data_size"])
        chunk_data_view = memoryview(chunk_array).cast("B").cast("f")
        chunk_prim = chunk_geom.modify_primitive(0)
        prim_array = chunk_prim.modify_vertices()
        chunk_prim_size = prim_array.get_num_rows()
        prim_array.set_num_rows(chunk_prim_size + tile_data["prim_size"])
        chunk_prim_view = memoryview(prim_array).cast("B").cast("I")

        chunk_data_view[chunk_data_size * self.data_stride:] = tile_data["data_view"]
        mat = Mat4.translate_mat(*pos)
        start = chunk_data_size
        end = start + tile_data["data_size"]
        chunk_data.transform_vertices(mat, start, end)
        chunk_prim_view[chunk_prim_size:] = tile_data["prim_view"]
        start = chunk_prim_size
        end = start + tile_data["prim_size"]
        chunk_prim.offset_vertices(chunk_data_size, start, end)

Your generate method would then become like this:


    def generate(self, size, min_height, max_height, roughness):

        ...

        ground_node = self.render.attach_new_node("GroundNode")

        # the number of float values used for the data of a single vertex
        self.data_stride = 8  # 3 coordinates + 3 normal components + 2 UVs

        # figure out some way to identify and select the different chunks
        for i in range(number_of_chunks):
            chunk_id = i
            self.create_chunk(chunk_id, ground_node)

        # store the data needed to fill in a chunk mesh
        tile_data = {}
        tile_types = ("water", "sand", "dirt", "grass", "plains", "rock", "snow")

        # load and process the tile models
        for tile_type in tile_types:
            model_path = f"Assets/Models/GroundTypes/{tile_type.title()}"
            data_view, prim_view, data_size, prim_size = self.load_tile(model_path)
            tile_data[tile_type] = {
                "data_view": data_view,
                "prim_view": prim_view,
                "data_size": data_size,
                "prim_size": prim_size
            }

        # fill the chunks
        for i in range(number_of_chunks):

            chunk_id = i
            chunk = ground_node.find(f"**/{chunk_id}")

            for y in range(1, size):
                for x in range(1, size):
                    tile_type = ""
                    if grid[x, y] < max_height * evo_config.WATER_LIMIT:
                        tile_type = "water"
                    elif grid[x, y] < max_height * evo_config.SAND_LIMIT:
                        tile_type = "sand"
                    elif grid[x, y] < max_height * evo_config.DIRT_LIMIT:
                        tile_type = "dirt"
                    elif grid[x, y] < max_height * evo_config.GRASS_PLAINS_LIMIT:
                        rnd = round(random.random() * 10)
                        if rnd < 5:
                            tile_type = "grass"
                        else:
                            tile_type = "plains"
                    elif grid[x, y] < max_height * evo_config.ROCK_LIMIT:
                        tile_type = "rock"
                    else:
                        tile_type = "snow"

                    if tile_type:
                        if tile_type != "water":
                            pos = (x, y, grid[x, y])
                        else:
                            pos = (x, y, min_z)
                        self.add_tile(tile_data[tile_type], chunk, pos)

        detailed_grid = solid_grid.gen_grid_from_terrain(grid, max_height)

        return ground_node, grid, detailed_grid

The code above hasn’t been tested, but it is based on the code I provided in this topic which did seem to work well.

Yeah it’s a lot to take in, but take your time to look it over; hopefully it will prove useful to you :slight_smile: .

You’re very welcome :slight_smile: !
If you have any questions concerning the code, just ask!