Questions concerning large terrain mesh

I’ve been pondering for some time what approach to take when starting to create a large world terrain mesh.
No need to think about other objects (I do and have digested already the Level Of Detail aspect for those).

What I’m thinking is the actual world mesh.

1.Would it be good to split world in to chunks of objects? For example 100 by 100 meters per object?
In prototype I simply had a object (parent) that contains all the chunk objects using Compound to create a single unified collision object. I simply then have a LOD put in place for each chunk, so they are not drawn if too far away.

  1. What to take account if using a single mesh/object for landscape if such is possible?
    I saw and tried this http://www.youtube.com/watch?v=wV_A8AAp_gs
    method but it has some issues when creating the world outside gameplay, so it is not useful for me.

I know this is a vague question but I would love to get some discussion of this.

I think the best way is to set up chunks with LoD, as you have done, and re-use the same chunks as many times as possible (also make as many tile-able as possible). Then do not use the terrain for collision. Use a bounding collision mesh for the terrain for each chunk (or maybe for every 8 chunks squared since it should be pretty low-poly). In addition to this, you could also set up ghost objects (halo planes that look like an object) for terrain far in the distance rather than having low-poly meshes.

I’ve made a script that does exactly that, subdivides a terrain mesh in a grid of objects with one operator. And another operator to assign LoD recursively to each part. The first operator works fine, however with the second there is a problem.

If you have a grid of meshes, and some of these meshes are low-poly (because of LOD) and some others not, when you put it all together you will see the “separations” in each mesh. There are two solutions. Make the low-poly meshes have hight poly marges (but losing some of the power of LOD), or edit the vertex one by one, but that must be done inside the BGE, python can’t handle it. Another option could be to use the LOD Shader for the graphics and this method for the physics. However this have also a problem. LOD shader don’t work (or work bad) with some graphic cards, people with these cards will not be able to play your game at all.

Now that I think about it again, if you make your own LOD system in which the low-poly meshes were joined, being bigger than the height-poly and with height poly marges. In this case you could retain almost all the power of LoD without see the separations betwern meshes. That could work, but again, it’s better to make this stuff inside the blender source code.

I let the script here, if you wanna try:


import bpy

#NOTES
# 1. Make sure you've selected first the cutter and then the map.
# 2. Execute the script throght the operator: "Split Map using a Grid"
# 3. Be sure that any object has the same name as the map, an object
#    like mymap.001 will be taked as the first minimap of mymap. 

#OPTIONS
grid = 3

class recursive_link_lod(bpy.types.Operator):
    bl_idname = "object.recursive_link_lod"
    bl_label = "Recursive Link LOD copies."

    def __init__(self):
        self.base = []
        self.nobase = [[]]
        
        #bpy.ops.object.mode_set(mode = 'OBJECT')
        selected = bpy.context.selected_editable_objects
        
        for object in selected:
            if not 'lod' in object.id_data.name:
                self.base.append(object)
            else:
                point = object.id_data.name.rfind(".")
                lod_point = object.id_data.name.rfind("lod")
                lod_index = int(object.id_data.name[lod_point+3:point]) - 1
                self.resize_nobase(lod_index)
                self.nobase[lod_index].append(object)

    #Fill nobase with empty lists if the index is out of range.
    def resize_nobase(self, index):
        size = len(self.nobase) - 1  
        if size >= index: return
        for n in range(index - size):
            self.nobase.append([])
        
    def execute(self, context):

        n = -1
        for lodlist in self.nobase:
            n+=1
            for lodob in lodlist:
                point = lodob.id_data.name.rfind(".")
                lod_point = lodob.id_data.name.rfind("lod")
                index = lodob.id_data.name[point+1:]
                name = lodob.id_data.name[:lod_point]
                
                #bpy.ops.object.mode_set(mode = 'OBJECT')
                bpy.ops.object.select_all(action='DESELECT')
                
                object = bpy.context.scene.objects[name + '.' + index]
                object.select = True
                bpy.context.scene.objects.active = object
                
                bpy.ops.object.lod_add()
                
                object.lod_levels[n].object = lodob
                object.lod_levels[n].use_mesh = True
                object.lod_levels[n].use_material = True   

                
        return {'FINISHED'}
        
class splitGrid(bpy.types.Operator):
    bl_idname = "mesh.split_grid"
    bl_label = "Split Map using a Grid"
    
    def __init__(self):
        try:
            bpy.ops.object.mode_set(mode = 'OBJECT')
            self.map = bpy.context.active_object
            selected = bpy.context.selected_editable_objects
            selected.remove(self.map)
            self.obj = selected[0]
            self.size = self.obj.dimensions
            self.location_x = self.obj.location[0]
            self.location_y = self.obj.location[1]

        except (ValueError, IndexError) as error:
            print("You need to select the cutter first and then the map.")
            self.init_error = True
            return
        
        self.init_error = False
        
    def execute(self, context):
        if self.init_error: return {'FINISHED'}
        
        self.setMiniCutter()
        self.createMinimaps()
        
        #Return to default
        bpy.ops.object.mode_set(mode = 'OBJECT')
        self.obj.scale = [self.obj.scale[0]*grid, self.obj.scale[1]*grid, self.obj.scale[2]*grid]
        self.obj.location[0] = self.location_x
        self.obj.location[1] = self.location_y
        return {'FINISHED'}

    def setMiniCutter(self):
        self.obj.scale = [self.obj.scale[0]/grid, self.obj.scale[1]/grid, self.obj.scale[2]/grid]


    def createMinimaps(self):
        sume = self.size[0]/(grid)
        count = 1
        for n in range(grid):
            for i in range(grid):
                #Print info
                print("Generating... " + str(count) + " of " + str(grid*grid))
                count+=1 

                #Set location
                nM = (n-(grid/2-0.5))
                iM = (i-(grid/2-0.5))
                
                self.obj.location[0] = nM*sume+self.location_x
                self.obj.location[1] = -iM*sume+self.location_y

                #Knife Project
                bpy.ops.object.mode_set(mode = 'EDIT')
                bpy.ops.mesh.select_all(action='DESELECT')
                bpy.ops.mesh.knife_project(cut_through=True)
                bpy.ops.mesh.separate()
                
                #set origin
                bpy.ops.object.mode_set(mode = 'OBJECT')
                name = self.map.id_data.name
                name += "." + self.suit_id((n*grid+i)+1)
                minimap = bpy.context.scene.objects[name]
                self.set_origin(minimap, [self.obj.location[0], self.obj.location[1], self.map.location[2]])        

    #Get the string of the minimap id.
    def suit_id(self, id):
        if id < 10: return "00" + str(id)
        if id < 100: return "0" + str(id)
        return str(id)
    
    #Send the origen to the right place on a minimap
    def set_origin(self, object, location):
        bpy.ops.object.select_all(action='DESELECT')
        object.select = True
        bpy.context.scene.objects.active = object
        
        bpy.context.scene.cursor_location = location
        bpy.ops.object.origin_set(type='ORIGIN_CURSOR')
        
        bpy.ops.object.select_all(action='DESELECT')
        self.obj.select = True
        bpy.context.scene.objects.active = self.map
        
    
def register():
    print("Script installed.")
    bpy.utils.register_class(splitGrid)
    bpy.utils.register_class(recursive_link_lod)


def unregister():
    print("Script uninstalled.")
    bpy.utils.unregister_class(splitGrid)
    bpy.utils.unregister_class(recursive_link_lod)

if __name__ == "__main__":
    register()