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

Here is a kinda large chunk of code that generates the models for each ground tile of a diamond-square powered terrain generator.

It works…but I am wondering if it is efficient at all? It generates a 30 * 30 map in about 3 seconds. A 100 * 100 takes much longer and my FPS drops a whole lot with that big of a map. Is a map of that size with that many ground tiles just too big or is there a way to better this code? Thanks for taking the time to look at this (if you do)!

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

    grid = diamond_square.make_grid(shape=(size, size),
                                    min_height=min_height,
                                    max_height=max_height,
                                    roughness=roughness)

    cam_max_z = 0
    min_z = 999999
    for y in range(1, size):
        for x in range(1, size):

            if grid[x, y] < min_z:
                if grid[x, y] > max_height * evo_config.WATER_LIMIT:
                    min_z = grid[x, y]

            if grid[x, y] > cam_max_z:
                cam_max_z = grid[x, y]

    cam.set_z(self, cam_max_z)

    ground_node = NodePath("GroundNode")

    n = None

    water = self.loader.loadModel("Assets/Models/GroundTypes/Water")
    water.clearModelNodes()
    sand = self.loader.loadModel("Assets/Models/GroundTypes/Sand")
    sand.clearModelNodes()
    dirt = self.loader.loadModel("Assets/Models/GroundTypes/Dirt")
    dirt.clearModelNodes()
    grass = self.loader.loadModel("Assets/Models/GroundTypes/Grass")
    grass.clearModelNodes()
    plains = self.loader.loadModel("Assets/Models/GroundTypes/Plains")
    plains.clearModelNodes()
    rock = self.loader.loadModel("Assets/Models/GroundTypes/Rock")
    rock.clearModelNodes()
    snow = self.loader.loadModel("Assets/Models/GroundTypes/Snow")
    snow.clearModelNodes()

    is_water = False
    for y in range(1, size):
        for x in range(1, size):
            if grid[x, y] < max_height * evo_config.WATER_LIMIT:
                n = ground_node.attachNewNode(f"Ground-{x},{y}")
                water.instanceTo(n)
                is_water = True
            elif grid[x, y] < max_height * evo_config.SAND_LIMIT:
                n = ground_node.attachNewNode(f"Ground-{x},{y}")
                sand.instanceTo(n)
            elif grid[x, y] < max_height * evo_config.DIRT_LIMIT:
                n = ground_node.attachNewNode(f"Ground-{x},{y}")
                dirt.instanceTo(n)
            elif grid[x, y] < max_height * evo_config.GRASS_PLAINS_LIMIT:
                rnd = round(random.random() * 10)
                if rnd < 5:
                    n = ground_node.attachNewNode(f"Ground-{x},{y}")
                    grass.instanceTo(n)
                else:
                    n = ground_node.attachNewNode(f"Ground-{x},{y}")
                    plains.instanceTo(n)
            elif grid[x, y] < max_height * evo_config.ROCK_LIMIT:
                n = ground_node.attachNewNode(f"Ground-{x},{y}")
                rock.instanceTo(n)
            else:
                n = ground_node.attachNewNode(f"Ground-{x},{y}")
                snow.instanceTo(n)

            if n is not None:
                if not is_water:
                    n.setPos(x, y, grid[x, y])
                else:
                    n.setPos(x, y, min_z)

            is_water = False

    ground_node.flattenStrong()
    ground_node.reparentTo(self.render)

    detailed_grid = solid_grid.gen_grid_from_terrain(grid, max_height)

    return ground_node, grid, detailed_grid

This is not a very efficient way to do it; it would be more efficient to learn how the GeomVertexData classes, etc. work and generate the tiles that way.

Thanks! I’ll look into it.

I kinda have a problem with getting excited and jumping into coding without reading the docs enough. Panda has been super cool and I’ve been able to do things much quicker than in RayLib, MonoGame, Love2D, Unity, Unreal, and Phaser. Lol. Learning python as I go has slowed me down a bit.

I’ll start looking at the “Advanced operations with internal structures” section in the Docs. This seems similar to what I was doing in RayLib with the same project I’m now trying in Panda. There was a bug in RayLib down deep in the OpenGL which was over my head to fix and most the contributors were into the 2d aspect more so I decided to look around for another framework/engine.

Anyway, too much typing. Thanks again.

Since you like jumping into the deep end anyway, you might also want to take a look at my proposed manual page about Using memoryviews. The use of memoryviews is much preferred over GeomVertexReaders/Writers when procedurally generating and/or altering geometry, but it might be a bit daunting for new Panda users. That’s why I wrote that manual page, in the hope that it will make the rather advanced techniques a bit easier to grasp. In case you decide to check it out, do let me know if you found it useful :slight_smile: .

I wanted to clarify, this is much faster than in C++, I do not understand the PR of this method.

Your manual page is a wee big complicated. Could you provide maybe an high concept overview of the steps required to do what I’m attempting.

It seems they are something along the lines of, and I could be way off:

  • Load 1 single ground tile model
  • Create an empty array for all my vertices using the # of vertices in my model
  • Loop through my model vertices and add them into a numpy array or ‘import array’ array
  • Somehow use this array to create the mesh which will be rendered
  • Change the color of the top few vertices to appear as different types of ground

That is way high level since I’ve only had a bit of time to look over this. This weekend I will have more time. I’m just hoping to get a decent amount of ammo before tackling the problem. If it sounds like I have no clue to what I’m talking about with what you’re proposing: memoryviews, then please let me know so I can try a different method. But I’d love to figure this out!

By the way, where can I look up what GeomVertexFormat.get_v3n3c4t2 these are? I’m kinda thinking this is declaring to use 3 spots of position, 3 spots for normals, 4 spots for color and…2 spots texture uv? lol

This is a really cool community, I’m thankful for all your suggestions!

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!

Hey, some I’m trying to get a good understanding of how to create meshes before I get to deep into the code you provided. Here I am trying to load a model, extract it’s info and rebuild it then render it. I’m a bit stuck at the primitive part as you can see below:

    # Load Model
    grass_model = self.loader.load_model("../Assets/Models/GroundTypes/Grass").children[0]

    # Get Geometry from model
    grass_geom = grass_model.node().modify_geom(0)

    # Get all vertex data from model's geometry
    grass_vertex_data = grass_geom.modify_vertex_data()

    # New vertex data
    ground_vertex_data = GeomVertexData('Ground', grass_vertex_data.format, Geom.UHStatic)

    # Create all needed tiles
    tile_count = 1
    ground_vertex_data.setNumRows(grass_vertex_data.getNumRows() * tile_count)

    # Loop and create one tile
    vertex_reader = GeomVertexReader(grass_vertex_data, 'vertex')
    vertex_writer = GeomVertexWriter(ground_vertex_data, 'vertex')

    normal_reader = GeomVertexReader(grass_vertex_data, 'normal')
    normal_writer = GeomVertexWriter(ground_vertex_data, 'normal')

    while not vertex_reader.isAtEnd():

        v = vertex_reader.getData3()
        vertex_writer.addData3(v)

        n = normal_reader.getData3()
        normal_writer.addData3(n)

    prim = GeomTriangles(Geom.UHStatic)
    prim.add_consecutive_vertices(0, grass_vertex_data.getNumRows()) # HERE IS WHERE I AM CONFUSED

    geom = Geom(ground_vertex_data)
    geom.addPrimitive(prim)

    node = GeomNode('GNode')
    node.addGeom(geom)

    node_path = self.render.attachNewNode(node)

mesh

Looking at the image, I’ve obviously done something wrong…

Ah, indeed it’s not that straightforward. The number of “vertices” (these are actually vertex indices) in the primitive is not always the same as the amount of vertices created (i.e. the number of rows in the vertex data table).
To illustrate this, consider a square, made up of 4 vertices. The GeomTriangles primitive needs to consist of, well, triangles. That means you need to add a number of indices to it that is a multiple of 3; in this case 2 triangles make up the square, thus 6 vertex indices in total need to be added to the primitive, for instance:

    prim.add_vertices(0, 1, 2)
    prim.add_vertices(0, 2, 3)

So you can’t simply use the add_consecutive_vertices method to add the indices all at once as a sequence of consecutive numbers (since some of them repeat), and their number always has to be a multiple of 3.

To correctly copy existing primitives, you can once again use GeomVertexReader/Writers:

    grass_prim = grass_geom.modify_primitive(0)
    grass_indices = grass_prim.modify_vertices()
    ground_indices = prim.modify_vertices()
    index_reader = GeomVertexReader(grass_indices, 0)
    index_writer = GeomVertexWriter(ground_indices, 0)

    while not index_reader.is_at_end():

        i = index_reader.get_data1i()
        index_writer.add_data1i(i)

When you add another copy, the index to write has to be increased by the total number of vertices in the copied model, multiplied by the number of previously added copies (assuming all tiles have the same number of vertices), e.g.:

    vert_count = grass_vertex_data.get_num_rows()
    copy_count = 3

    # add the 4th copy
    total_vert_count = index_count * copy_count
    while not index_reader.is_at_end():

        i = index_reader.get_data1i()
        index_writer.add_data1i(i + total_vert_count)

or you can offset the recently added indices all at once:

    index_count = grass_prim.get_num_vertices()
    total_index_count = index_count * copy_count
    start = total_index_count
    end = start + index_count
    prim.offset_vertices(total_vert_count, start, end)

So this works.

    # Load Model
    grass_model = self.loader.load_model("../Assets/Models/GroundTypes/Ground").children[0]

    # Get Geometry from model
    grass_geom = grass_model.node().modify_geom(0)

    # Get all vertex data from model's geometry
    grass_vertex_data = grass_geom.modify_vertex_data()
    total_vert_count = tile_count * grass_vertex_data.get_num_rows()

    # Create future primitive
    prim = GeomTriangles(Geom.UHStatic)

    # New vertex data
    array = GeomVertexArrayFormat()
    array.addColumn("vertex", 3, Geom.NTFloat32, Geom.CPoint)
    array.addColumn("normal", 3, Geom.NTFloat32, Geom.CPoint)
    array.addColumn("color", 4, Geom.NTFloat32, Geom.CColor)

    ground_format_data = GeomVertexFormat()
    ground_format_data.addArray(array)

    ground_format_data = GeomVertexFormat.registerFormat(ground_format_data)

    ground_vertex_data = GeomVertexData('Ground', ground_format_data, Geom.UHStatic)
    ground_vertex_data.setNumRows(total_vert_count)

    # Loop and create one tile
    vertex_reader = GeomVertexReader(grass_vertex_data, 'vertex')
    vertex_writer = GeomVertexWriter(ground_vertex_data, 'vertex')

    normal_reader = GeomVertexReader(grass_vertex_data, 'normal')
    normal_writer = GeomVertexWriter(ground_vertex_data, 'normal')

    color_writer = GeomVertexWriter(ground_vertex_data, 'color')

    grass_prim = grass_geom.modify_primitive(0)
    grass_indices = grass_prim.modify_vertices()
    ground_indices = prim.modify_vertices()
    index_reader = GeomVertexReader(grass_indices, 0)
    index_writer = GeomVertexWriter(ground_indices, 0)

    tile_verts = []
    tile_norms = []
    tile_colors = []
    tile_indices = []

    while not vertex_reader.isAtEnd():

        v = vertex_reader.getData3()
        tile_verts.append(v)

        n = normal_reader.getData3()
        tile_norms.append(n)

        tile_colors.append(evo_config.get_water_color(1, 10))

    while not index_reader.is_at_end():
        i = index_reader.get_data1i()
        tile_indices.append(i)

    for i in range(0, len(tile_verts)):

        vertex_writer.addData3(tile_verts[i])
        normal_writer.addData3(tile_norms[i])
        color_writer.addData4(tile_colors[i])

    for i in range(0, len(tile_indices)):

        index_writer.add_data1i(tile_indices[i])

    geom = Geom(ground_vertex_data)
    geom.addPrimitive(prim)

    node = GeomNode('GNode')
    node.addGeom(geom)

    node_path = self.render.attachNewNode(node)

But I have no idea how to loop and make a grid of these cubes which are made. I think the memoryview method is too complicated as it involves python (which I’m learning still) and also a complexity that I am not ready for.

I am starting to understand how to create meshes with the geom reader/writers, I was even able to add a color :slight_smile: lol but like I said I don’t know how to either append another set of vertices that represents the second tile in the ground grid OR create another primitive at a different position and at it to the geom…

To improve the FPS for large maps, do I have to create one primitive that contains all the vertices of all the ground tiles or can I have a bunch of primitives(one primitive for each ground cube/tile) added to a geom?

I made these:

    tile_verts = []
    tile_norms = []
    tile_colors = []
    tile_indices = []

In hopes I could just loop through a few times and make a bunch of primitives to add to my geom but I’m not sure how to offset the position of the x and y vertices so that the ground is formed…

Sorry, it’s taking so long for me to get this…

Just btw, I was able to get the memory view code working but the FPS dropped to 15 on a map size that normally is 60 FPS

Fair enough; start with whatever you feel most comfortable with :slight_smile: .

That probably doesn’t make such a big difference, although it’s always best to have as few different geometry-related objects as possible, I believe.

That’s very good; we can indeed use those to add that tile geometry to the new ground geometry multiple times and offset the new vertices each time with a call to GeomVertexData.transform_vertices so the vertices end up in the desired location.
Let’s keep things simple for now and pretend there’s only one type of tile and thus only one set of vertex data (positions, normals and color):

    # at this point, no tiles were added yet
    vert_count = grass_vertex_data.get_num_rows()
    total_vert_count = 0
    index_count = grass_prim.get_num_vertices()
    total_index_count = 0

    # let's say we already have all of the tile positions, stored in a list;
    # for each of those positions we will now add new tile geometry to the
    # ground geometry and offset its vertices accordingly
    for pos in self.tile_positions:

        for i in range(len(tile_verts)):

            vertex_writer.addData3(tile_verts[i])
            normal_writer.addData3(tile_norms[i])
            color_writer.addData4(tile_colors[i])

        for index in tile_indices:

            index_writer.add_data1i(index)

        start = total_index_count
        total_index_count += index_count
        # offset the vertex indices so they refer to the newly added vertices
        prim.offset_vertices(total_vert_count, start, total_index_count)

        mat = Mat4.translate_mat(*pos)
        start = total_vert_count
        total_vert_count += vert_count
        # translate all of this tile's vertices so the entire tile ends up at `pos`
        ground_vertex_data.transform_vertices(mat, start, total_vert_count)

That’s OK, don’t worry about it. This is after all quite complicated stuff you’re trying to tackle! One step at a time :slight_smile: .

That’s most likely due to Panda not being able to cull anything; when at least one part of the map is in view, the entire thing gets rendered. With the separate tiles, those outside of the view can get culled (so they’re not rendered). Dividing the map into several large “chunks” might improve things, but that’s something I have no experience with myself; perhaps others can chime in on this.

So thanks to your help I was able to get multiple blocks rendering and am pretty sure I can now do the grid alone but I do have one problem that I can’t figure out.

    tile_count = 4

    # Load Model
    grass_model = self.loader.load_model("../Assets/Models/GroundTypes/Ground").children[0]

    # Get Geometry from model
    grass_geom = grass_model.node().modify_geom(0)

    # Get all vertex data from model's geometry
    grass_vertex_data = grass_geom.modify_vertex_data()
    total_vert_count = tile_count * grass_vertex_data.get_num_rows()

    # Create future primitive
    prim = GeomTriangles(Geom.UHStatic)

    # New vertex data
    array = GeomVertexArrayFormat()
    array.addColumn("vertex", 3, Geom.NTFloat32, Geom.CPoint)
    array.addColumn("normal", 3, Geom.NTFloat32, Geom.CPoint)
    array.addColumn("color", 4, Geom.NTFloat32, Geom.CColor)

    ground_format_data = GeomVertexFormat()
    ground_format_data.addArray(array)

    ground_format_data = GeomVertexFormat.registerFormat(ground_format_data)

    grass_prim = grass_geom.modify_primitive(0)
    grass_indices = grass_prim.modify_vertices()
    ground_indices = prim.modify_vertices()

    # Open readers
    vertex_reader = GeomVertexReader(grass_vertex_data, 'vertex')
    normal_reader = GeomVertexReader(grass_vertex_data, 'normal')
    index_reader = GeomVertexReader(grass_indices, 0)

    tile_verts = []
    tile_norms = []
    tile_colors = []
    tile_indices = []

    while not vertex_reader.isAtEnd():

        v = vertex_reader.getData3()
        tile_verts.append(v)

        n = normal_reader.getData3()
        tile_norms.append(n)

        tile_colors.append(evo_config.get_water_color(1, 10))

    while not index_reader.is_at_end():
        i = index_reader.get_data1i()
        tile_indices.append(i)

    ground_vertex_data = GeomVertexData('Ground', ground_format_data, Geom.UHStatic)
    ground_vertex_data.setNumRows(total_vert_count)

    # Open writers
    vertex_writer = GeomVertexWriter(ground_vertex_data, 'vertex')
    normal_writer = GeomVertexWriter(ground_vertex_data, 'normal')
    color_writer = GeomVertexWriter(ground_vertex_data, 'color')
    index_writer = GeomVertexWriter(ground_indices, 0)

    geom = None

    total_index_count = 0
    total_vert_count = 0
    grass_index_count = grass_prim.get_num_vertices()
    grass_vert_count = grass_vertex_data.get_num_rows()

    for tc in range(0, tile_count):

        for i in range(0, len(tile_verts)):

            vertex_writer.addData3(tile_verts[i])
            normal_writer.addData3(tile_norms[i])
            color_writer.addData4(tile_colors[i])

        for i in range(0, len(tile_indices)):

            index_writer.add_data1i(tile_indices[i])

        start = total_index_count
        total_index_count += grass_index_count

        prim.offsetVertices(total_vert_count, start, total_index_count)

        trans_matrix = Mat4.translateMat(tc * 2, 0, 0)

        start = total_vert_count
        total_vert_count += grass_vert_count

        ground_vertex_data.transformVertices(trans_matrix, start, total_vert_count)

        geom = Geom(ground_vertex_data)
        geom.addPrimitive(prim)

    node = GeomNode('GNode')
    node.addGeom(geom)

    node_path = self.render.attachNewNode(node)

This produces:

So it’s working :slight_smile: but if I increase the tile count to, say 50, I get this error:

Known pipe types:
wglGraphicsPipe
(all display modules loaded.)
Assertion failed: end_row <= get_num_vertices() at line 486 of c:\buildslave\sdk-windows-amd64\build\panda\src\gobj\geomPrimitive.cxx
Traceback (most recent call last):
File “/gen_ground_testing.py”, line 135, in
game = Game()
File “/gen_ground_testing.py”, line 117, in init
prim.offsetVertices(total_vert_count, start, total_index_count)
AssertionError: end_row <= get_num_vertices() at line 486 of c:\buildslave\sdk-windows-amd64\build\panda\src\gobj\geomPrimitive.cxx

I need it to be able to work with somewhat large numbers. So, I’m hoping it’s possible to increase that tile_count a bunch.

Well that’s surprising. This seems caused by the default index data type of a GeomPrimitive, which is GeomEnums.NT_uint16 and doesn’t allow more than 65536 indices to be added. However, upon investigating this issue, it seems that many more indices are nevertheless added (the index type is presumably changed automatically when this limit is exceeded using a GeomVertexWriter), until offsetting these indices somehow causes an internal problem apparently. This might be a bug in Panda, not sure.

Anyway, it seems fixed by manually setting the index type to GeomEnums.NT_uint32:

    # Create future primitive
    prim = GeomTriangles(GeomEnums.UH_static)
    prim.set_index_type(GeomEnums.NT_uint32)

Now you should be able to create pretty big maps :slight_smile: .

Some remarks about the vertex data format you defined:

  • for normals, you should use GeomEnums.C_normal as contents type. Otherwise they will not be automatically updated when you apply e.g. non-uniform scaling to the model (and calling transform_vertices would transform them as well–which should not happen; you can already see in the screenshot you posted that the shading gets worse for each added tile).
  • For colors, it is more memory-efficient to use GeomEnums.NT_uint8 as numeric type. If you were using memoryviews, this would require adding colors as integers in the [0, 255] range, but GeomVertexWriters make that conversion automatically, so you can keep adding floats in the [0.0, 1.0] range.

On a side note, if you were wondering why I tend to use the snake_case version of Panda method names in my code, the reason is that there was talk of the camelCase names getting phased out at some point, so I prefer to make my code more “future-proof” that way.

Thank you for all your help! Everything is up and running. I had to tweak the lighting a bit to get the FPS where I wanted but I’m happy with the look.

That’s great to hear! Glad I could help :slight_smile: .