Terrain's edit tiles system

Example of tiles implementation in terrane. You have two arrays (storage_chunk and terrain_chunk), one does not participate in rendering, it just stores data as a DataChunk class. The second one is actually a NodePath repository, it participates in rendering.

The update method of the Terrain class calculates the coordinates of fragments around the spectator (camera) based on the position and checks if there are fragments with such coordinates in storage_hunk. If there are any, it adds such a fragment for rendering.

Use right mouse button to look around.
Use movement in direction of view - W
Use moving back up - S
Use speed up - Shift

Use left mouse to change color of tile.

from direct.showbase.ShowBase import ShowBase

from panda3d.core import (Vec2, Vec4, Vec3, Point3, GeomVertexData, GeomVertexFormat, Geom, GeomTriangles, GeomVertexWriter, 
GeomNode, Texture, TextureAttrib, NodePath, RenderState, Material, MaterialAttrib, Plane)

import random

CHUNK_SIZE = 2
MAX_VIEW_DIST = 50

vdata = GeomVertexData('name', GeomVertexFormat.get_v3n3t2(), Geom.UHStatic)
vdata.set_num_rows(3)

vertex = GeomVertexWriter(vdata, 'vertex')
normal = GeomVertexWriter(vdata, 'normal')
texcoord = GeomVertexWriter(vdata, 'texcoord')

vertex.add_data3(-1, -1, 0)
vertex.add_data3(1, -1, 0)
vertex.add_data3(1, 1, 0)
vertex.add_data3(-1, 1, 0)

normal.add_data3(0, 0, 1)
normal.add_data3(0, 0, 1)
normal.add_data3(0, 0, 1)
normal.add_data3(0, 0, 1)

texcoord.add_data2(0, 0)
texcoord.add_data2(1, 0)
texcoord.add_data2(1, 1)
texcoord.add_data2(0, 1)

prim = GeomTriangles(Geom.UHStatic)
prim.add_vertices(0, 1, 2)
prim.add_vertices(0, 2, 3)
prim.close_primitive()

blank_geom = Geom(vdata)
blank_geom.add_primitive(prim)

blank_mat = Material("Material1")

class Chunk:
    def __init__(self, tiles_pos, data_chunk):
        self.position = tiles_pos * CHUNK_SIZE

        state = RenderState.make(MaterialAttrib.make(blank_mat))

        geom_node = GeomNode('tile:{}'.format(tiles_pos))
        geom_node.add_geom(blank_geom, state)

        self.model = NodePath(geom_node)
        self.model.hide()
        self.model.reparent_to(render)
        self.model.set_pos(self.position.x, self.position.y, 0)
        self.model.set_color(data_chunk.color)

    def update(self, spectator):
        if spectator.get_distance(self.model) <= MAX_VIEW_DIST:
            if self.model.is_hidden():
                self.model.show()
        else:
            if not self.model.is_hidden():
                self.model.hide()

class Terrain:
    def __init__(self):
        self.view_dst = round(MAX_VIEW_DIST/CHUNK_SIZE)
        self.spectator = NodePath("None")
        self.storage_сhunk = {}
        self.terrain_сhunk = {}

    def set_сhunk_color(self, tiles_pos, color):
        if tiles_pos in self.storage_сhunk:
            self.storage_сhunk[tiles_pos].color = color
            if tiles_pos in self.terrain_сhunk:
                self.terrain_сhunk[tiles_pos].model.set_color(color)

    def add_сhunk_storage(self, pos, geom):
        self.storage_сhunk[pos] = geom

    def del_сhunk_storage(self, pos):
        self.storage_сhunk.pop(pos)

    def update(self):
        spectator_coord_x = round(self.spectator.get_x()/CHUNK_SIZE)
        spectator_coord_y = round(self.spectator.get_y()/CHUNK_SIZE)

        yOffset = -self.view_dst
        while yOffset <= self.view_dst:
            xOffset = -self.view_dst
            while xOffset <= self.view_dst:
                tiles_pos = Vec2(spectator_coord_x + xOffset, spectator_coord_y + yOffset)
                if tiles_pos in self.storage_сhunk:
                    if tiles_pos in self.terrain_сhunk:
                        self.terrain_сhunk[tiles_pos].update(self.spectator)
                    else:
                        self.terrain_сhunk[tiles_pos] = Chunk(tiles_pos, self.storage_сhunk[tiles_pos])
                xOffset += 1
            yOffset += 1

for сhunk_сoord, сhunk in list(self.terrain_сhunk.items()):
    vec = Vec2(self.spectator.get_x(), self.spectator.get_y()) - сhunk.position
    if vec.length_squared() > 7000:
        self.terrain_сhunk.pop(сhunk_сoord)
        сhunk.model.remove_node()

class DataChunk():
    def __init__(self):
        self.color = None

class MyApp(ShowBase):

    def __init__(self):
        ShowBase.__init__(self)
        
        base.disable_mouse()
        camera.set_pos(0, 0, 5)
        self.camera_sensitivity = 0.05
        self.camera_speed = 6000

        self.model = loader.load_model("panda")
        self.model.reparent_to(render)

        self.plane = Plane(Vec3(0, 0, 1), Point3(0, 0, 0))

        self.keyMap = {"Mouse3":0, "FORWARD":0, "BACK":0, "LSHIFT":0}

        base.accept("mouse1", self.mouse1_click)
        base.accept("mouse3", self.set_key, ["Mouse3",1])
        base.accept("mouse3-up", self.set_key, ["Mouse3",0])
        base.accept("w", self.set_key, ["FORWARD",1])
        base.accept("w-up", self.set_key, ["FORWARD",0])
        base.accept("s", self.set_key, ["BACK",1])
        base.accept("s-up", self.set_key, ["BACK",0])
        base.accept("lshift", self.set_key, ["LSHIFT",1])
        base.accept("lshift-up", self.set_key, ["LSHIFT",0])

        self.terrain_system = Terrain()
        self.terrain_system.spectator = base.camera

        base.taskMgr.add(self.update_terrain_system, "update_terrain_system")
        base.taskMgr.add(self.cam_control, "cam_control")

        WORLD_MAP = 500
        for x in range((WORLD_MAP*2)+1):
            for y in range((WORLD_MAP*2)+1):
                data_chunk = DataChunk()
                data_chunk.color = Vec4(random.random(), random.random(), random.random(), 1)

                self.terrain_system.add_сhunk_storage(Vec2(x-WORLD_MAP, y-WORLD_MAP), data_chunk)

        HOLES = 2
        for x in range((HOLES*2)+1):
            for y in range((HOLES*2)+1):
                self.terrain_system.del_сhunk_storage(Vec2(x-HOLES, y-HOLES))

    def mouse1_click(self):
        if base.mouseWatcherNode.has_mouse():
            mpos = base.mouseWatcherNode.get_mouse()
            pos3d = Point3()
            nearPoint = Point3()
            farPoint = Point3()
            base.camLens.extrude(mpos, nearPoint, farPoint)
            if self.plane.intersectsLine(pos3d, 
                                         render.get_relative_point(camera, nearPoint),
                                         render.get_relative_point(camera, farPoint)):

                tiles_pos = Vec2(round(pos3d.get_x()/CHUNK_SIZE), round(pos3d.get_y()/CHUNK_SIZE))

                new_color = Vec4(1, 0, 0, 1)
                self.terrain_system.set_сhunk_color(tiles_pos, new_color)

    def set_key(self, key, value):
        self.keyMap[key] = value

    def update_terrain_system(self, task):
        self.terrain_system.update()
        return task.cont

    def cam_control(self, task):
        if (self.keyMap["Mouse3"] != 0):
            md = base.win.get_pointer(0)
            if base.win.move_pointer(0, base.win.get_x_size()//2, base.win.get_y_size()//2):
                camera.setH(camera.getH() - (md.get_x() - base.win.get_x_size()/2)*self.camera_sensitivity)
                camera.setP(camera.getP() - (md.get_y() - base.win.get_y_size()/2)*self.camera_sensitivity)

            dirFB = base.camera.get_mat().get_row3(1)

            camera_speed = self.camera_speed

            if (self.keyMap["LSHIFT"]!=0):
                camera_speed = self.camera_speed * 5

            if (self.keyMap["FORWARD"]!=0):
                camera.setPos(camera.get_pos()+dirFB*camera_speed*task.dt)

            if (self.keyMap["BACK"]!=0):
                camera.setPos(camera.get_pos()-dirFB*camera_speed*task.dt)

        return task.cont

app = MyApp()
app.run()

P.S.
The graph cleanup code is still quite slow, perhaps this part is best implemented asynchronously.

for сhunk_сoord, сhunk in list(self.terrain_сhunk.items()):
    vec = Vec2(self.spectator.get_x(), self.spectator.get_y()) - сhunk.position
    if vec.length_squared() > 3000:
        self.terrain_сhunk.pop(сhunk_сoord)
        сhunk.model.remove_node()

Add:
I think the distance of the object being deleted can be increased, this reduces access to the dictionary.

if vec.length_squared() > 7000:
2 Likes

Thank You very much! this is awesome code.

Thanks for the rating, by the way, the Rimworld camera is from above and static, so you can use the coordinates of the object relative to the center of the screen for culling. Calculating the distance is too expensive.

Thanks! for now i use Panda3d build-in camera for debugging and real in-game camera will come in a bit later, after all it’s more like a game loop sample now,
i have auto generating navmesh, sand textured geoms as surface, behaviors, and auto adjusted to each other walls. It’s still long road to prototype.