realtime - ptabriz/geodesign_with_blender GitHub Wiki

Real-time 3D modeling and coupling with GIS data

Contents:

Required software and materials

  • Download and install latest version of Blender from here.
  • Download and install Blender GIS addon from here. Installation guide available here.
  • Download and unpack realtime_tutorial_data.zip from here

I. Intro to coupling with Modal Timer

In this section we learn the basics to setup simple coupling for importing and processing of geospatial data, in realtime. We do that by setting up a monitoring system inside blender that continuously looks for incoming commands (e.g, through sockets), files (e.g, shape file, raster files, etc.), or user interaction (e.g, mouse, joystick, keyboard). In Blender this procedure is handled through a module called Modal Timer Operator. The reason that we focus on this specific module is that routine monitoring libraries like Watchmode or Threading are not well handled in Blender and often results in crashes. These modules interfere with blender's ability to run multiple operators at once and update different parts of the interface as the tool runs.

The data can be transferred locally or over network, using simple file copy or more advanced methods like sockets. As an example, the following video shows a real-time coupling with GRASS GIS. GrassGIS itself is paired with Kinect to scan the elevation and color changes in the physical model. As user interacts with the physical model, GRASS GIS runs various simulations, and exports them as raster and shape formats to a system directory. In Blender Modal timer is continuously monitoring the directory to update the model based on incoming data types. Those include terrain surface (Geotiff), ponding simulation (3Dpolygon), landcover patches (3D polygon), camera location (3Dpolyline), trail (3Dpolyline).


Blender Viewport Modal timer example

Example 1.

Lets take a peek at the module's components and functionality in the following example.

‣ Procedure

  • Open the file .realtime_tutorial/Modal.blend

  • Run the script that is loaded in the text editor

  • Select the Monkey object and move it around. You will see that as you are moving the object, three operations are running simultaneously: 1) the RGB values change, 2) a text object changes to show the updated RGB values, 3) and the timer text object changes to show the elapsed time in seconds.

  • Cancel the modal mode using "Esc" key.

  • Take a quick look at the commented code to check the modules components and their functionality

import bpy

class ModalTimerOperator(bpy.types.Operator):
    """Operator which runs its self from a timer"""
    bl_idname = "wm.modal_timer_operator"
    bl_label = "Modal Timer Operator"

    _timer = None

    def modal(self, context, event):

        # Terminate module when ESC key is pressed
        if event.type in {'ESC'}:
            self.cancel(context)
            return {'CANCELLED'}

        # assign timer condition to event handler
        if event.type == 'TIMER':

            # get the suzanne object's location #
            loc = bpy.data.objects ["Suzanne"].location
            # set the diffuse shader r,g,b to suzanne's x,y,z#
            self.a[0] = abs(loc[0]/5)
            self.a[1] = abs(loc[1]/5)
            self.a[2] = abs(loc[2]/5)
            print (self._timer.time_duration)

            #update the RGB text and location#
            colText = "RGB = {0}, {1}, {2}".format(str(round(self.a[0],2)),
                            str(round(self.a[1],2)),str(round(self.a[2],2)))                        
            self.rgb.data.body = colText
            self.rgb.location = (loc[0]+1,loc[1]-.5,loc[2]-1)

            #update the timer text and location#
            self.timer_text.location = (loc[0]+1,loc[1]+.5,loc[2]-1)
            self.timer_text.data.body = "Timer = " + str(round(self._timer.time_duration))

        return {'PASS_THROUGH'}

    def execute(self, context):
        wm = context.window_manager
        # Per seconds timer steps
        self._timer = wm.event_timer_add(1, context.window)
        wm.modal_handler_add(self)

        self.timer_text = bpy.data.objects ["Timer"]
        self.rgb = bpy.data.objects ["RGB"]
        # get the active object material #
        mat = bpy.data.materials.get("Material")
        # Get diffuse shader nodes's color #
        node_tree = mat.node_tree
        self.a = node_tree.nodes["Diffuse BSDF"].inputs[0].default_value

        return {'RUNNING_MODAL'}

    def cancel(self, context):
        wm = context.window_manager
        wm.event_timer_remove(self._timer)


def register():
    bpy.utils.register_class(ModalTimerOperator)


def unregister():
    bpy.utils.unregister_class(ModalTimerOperator)


if __name__ == "__main__":
    register()

    # test call
    bpy.ops.wm.modal_timer_operator()



II. Coupling with GIS data

In this example we are using modal timer to monitor a system directory, In the realtime_tutotrial_data folder you can see two folders named "Watch" and "scratch". The scratch folder contains 45 shape files and 45 images. The shapefiles represent viewpoints across a path, and textures represent viewsheds simulated from those locations. Viewsheds are combined with landcover to show the landuse composition of visible surface. Through a python script we setup modal timer to constantly look for files to import and process. To emulate the geospatial simulation we setup a second modal timer that copies the geospatial data from the Scratch folder to Watch folder (look at above scheme). The python script is consisted of the following python classes.

  • Locate and run coupling_example.blend in realtime_tutorial_data

1. adapt class processes the incoming files and scene objects. Specifically it performs the following operations.

  • Imports the viewshed map
  • Replaces the emission texture of the DSM object with the imported map
  • Imports the viewpoint shape file
  • Aligns the location of the viewshed marker (Torus object) with the location of the imported viewpoint.

2. Modal timer Looks into the Watch directory, detects the type of incoming file, sends them to adapt class and finally removes the file from the watch folder.
3. Modal_copy acts as a surrogate for your GIS software and copies texture and Point shape files from Scratch folder to the Watch folder to simulate the condition where your GIS application is automatically sending files over the network or locally. 4. Panel a small widget with buttons to run the modules (2 and 3)

‣ Procedure

  • Go to file ‣ preferences ‣ addons ‣ BlenderGIS ‣ import/Export panel
  • Unselect Adjust 3D view and Forced Textured solid shading.
  • Now run the script loaded into the text editor
  • The scripts adds a new panel in 3D view's toolbar (left side) with two buttons, Watch mode and Copy files
  • First Press Watch mode and then press Copy files
  • You should be able to see the viewshed maps and the observer location object updating along the path.



class adapt:
    ''' Adapt the scene objects based on viewpoint and texture files'''

    def __init__(self):
        self.terrain = "DSM"
        self.viewpoint = "Torus"
        filePath = os.path.dirname(bpy.path.abspath("//"))
        fileName = os.path.join(filePath,'vpoints.shp')

    def viewshed(self,texture,vpoint):
        ''' Recieve and process viewshed point shape file and texture '''

        ## import the shapefile, move viewmarker and delete the shapefile ##
        vpointDir = os.path.join (watchFolder,vpoint)
        vpointFile = vpointDir + "/" + vpoint + ".shp"
        if not bpy.data.objects.get(vpoint):
            bpy.ops.importgis.shapefile(filepath=vpointFile,fieldElevName="elev",
            shpCRS='EPSG:3358')

        bpy.data.objects[self.viewpoint].location = bpy.data.objects[vpoint].location
        shutil.rmtree(vpointDir)

        ## assign change terrain's texture file ##
        if bpy.data.objects.get(self.terrain):
            bpy.data.objects[self.terrain].select = True
            # remove the texture file from the directory
            os.remove(os.path.join(watchFolder,texture))

        # Change the material emmission shader texture #         
        if not bpy.data.images.get(texture):
            texpath = os.path.join(scratchFolder,texture)
            bpy.data.images.load(texpath)
        # get the active object material #
        mat = bpy.data.materials.get("Material")
        # Get material tree , nodes and links #
        nodes = mat.node_tree.nodes
        nodes.active = nodes[5]
        #Replace the texture #
        nodes[5].image = bpy.data.images[texture]
class Modal_watch(bpy.types.Operator):
        """Operator which interatively runs from a timer"""

        bl_idname = "wm.loose_coupling_timer"
        bl_label = "loose coupling timer"
        _timer = 0
        _timer_count = 0

        def modal(self, context, event):
            if event.type in {"RIGHTMOUSE", "ESC"}:
                return {"CANCELLED"}

            # this condition encomasses all the actions required for watching
            # the folder and related file/object operations

            if event.type == "TIMER":

                if self._timer.time_duration != self._timer_count:
                    self._timer_count = self._timer.time_duration
                    fileList = (os.listdir(watchFolder))
                    # Tree patches #
                    for fileName in fileList:
                        if ".png" in fileName:
                            vpoint = "viewpoint_" + fileName.split(".")[0]
                            if vpoint in fileList:
                                adapt().viewshed(fileName,vpoint)
                                self.adaptMode = "VIEWSHED"
            return {"PASS_THROUGH"}

        def execute(self, context):

            bpy.context.space_data.show_manipulator = False
            wm = context.window_manager
            wm.modal_handler_add(self)
            self._timer = wm.event_timer_add(3, context.window)
            self.adaptMode = None

            return {"RUNNING_MODAL"}

        def cancel(self, context):
            wm = context.window_manager
            wm.event_timer_remove(self._timer)
class Modal_copy(bpy.types.Operator):
        """Operator which interatively runs from a timer"""

        bl_idname = "wm.copy_files"
        bl_label = "copy files to watch folder"
        _timer = 0
        _timer_count = 0
        _index = 0


        def modal(self, context, event):
            if event.type in {"RIGHTMOUSE", "ESC"}:
                return {"CANCELLED"}

            if event.type == "TIMER":
                if self._index == len(self.copyList):
                    self._index = 0
                if self._timer.time_duration != self._timer_count:

                    if self.copyList and not os.listdir(watchFolder):
                        item = self.copyList[self._index]
                        if item not in self._copiedList:
                            fileSrc = os.path.join(scratchFolder, item[1])
                            fileDst = os.path.join(watchFolder, item[1])
                            dirSrc = os.path.join(scratchFolder, item[2])
                            dirDst = os.path.join(watchFolder, item[2])
                            shutil.copytree(dirSrc, dirDst)
                            shutil.copyfile(fileSrc, fileDst)
                            self._copiedList.append(item)
                            self._index += 1
            return {"PASS_THROUGH"}

        def execute(self, context):

            wm = context.window_manager
            wm.modal_handler_add(self)
            self._timer = wm.event_timer_add(1, context.window)
            fileList = os.listdir(scratchFolder)
            self._copiedList = []
            self.copyList = []
            for f in fileList:
                if ".png" in f:
                    vpointDir = "viewpoint_" + f.split(".")[0]

                    if vpointDir in fileList:
                        self.copyList.append((int(f.split(".")[0]),f,vpointDir))

            self.copyList = sorted(self.copyList)

            return {"RUNNING_MODAL"}

        def cancel(self, context):

            wm = context.window_manager
            wm.event_timer_remove(self._timer)
⚠️ **GitHub.com Fallback** ⚠️