Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add ability to animate shapefiles #511

Open
wants to merge 15 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
67 changes: 67 additions & 0 deletions operators/io_import_georaster.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@
import math
from mathutils import Vector
import numpy as np#Ship with Blender since 2.70
import glob
import re

import logging
log = logging.getLogger(__name__)
Expand Down Expand Up @@ -139,6 +141,12 @@ def listSubdivisionModes(self, context):
default=False
)
#
animate: BoolProperty(
name="Animate multiple DEMs",
description="Animate across other DEM rasters in the same folder",
default=False
)
#
step: IntProperty(name = "Step", default=1, description="Pixel step", min=1)

buildFaces: BoolProperty(name="Build faces", default=True, description='Build quad faces connecting pixel point cloud')
Expand Down Expand Up @@ -180,6 +188,7 @@ def draw(self, context):
layout.prop(self, 'buildFaces')
layout.prop(self, 'step')
layout.prop(self, 'clip')
layout.prop(self, 'animate')
if self.clip:
if geoscn.isGeoref and len(self.objectsLst) > 0:
layout.prop(self, 'objectsLst')
Expand Down Expand Up @@ -460,6 +469,64 @@ def execute(self, context):
obj = placeObj(mesh, name)
#grid.unload()

if self.animate:
log.info("Animate mode")
# Find all files in the same directory with the same extension
basedir = os.path.dirname(filePath)
file_extension = os.path.splitext(filePath)[1]
files = glob.glob(f"{basedir}{os.sep}*{file_extension}")
files.sort()
frames = []
# Try set the name of the shapekey based on the detected frame (number in filename)
try:
frame = int(re.search(r'\d+', name).group())
except:
frame = 1
frames.append(frame)
obj.shape_key_add(name=str(frame))
for f in files:
if f == filePath or f.endswith("_bgis.tif"):
continue
log.info(f"Loading {f}")
name = os.path.splitext(os.path.basename(f))[0]
try:
frame = int(re.search(r'\d+', name).group())
except:
frame += 1
frames.append(frame)
# Load file for frame, and copy the mesh data in as a shape key
try:
grid = GeoRaster(f, subBoxGeo=subBox, useGDAL=HAS_GDAL)
except IOError as e:
log.error("Unable to open raster", exc_info=True)
self.report({'ERROR'}, "Unable to open raster, check logs for more infos")
return {'CANCELLED'}
except OverlapError:
self.report({'ERROR'}, "Non overlap data")
return {'CANCELLED'}
new_mesh = exportAsMesh(grid, dx, dy, self.step, reproj=rprjToScene, subset=self.clip, flat=False, buildFaces=self.buildFaces)
obj.shape_key_add(name=str(frame))
if len(new_mesh.vertices) != len(mesh.vertices):
error = f"Different number of vertices - {len(new_mesh.vertices)} != {len(mesh.vertices)}"
print(error)
self.report({'ERROR'}, error)
return {'CANCELLED'}
for j, vertex in enumerate(new_mesh.vertices):
obj.data.shape_keys.key_blocks[str(frame)].data[j].co = vertex.co

frames.sort()
for k, frame in enumerate(frames):
# Create keyframes
sframe = str(frame)
if k > 0:
obj.data.shape_keys.key_blocks[sframe].value = 0.0
obj.data.shape_keys.key_blocks[sframe].keyframe_insert(data_path='value', frame=frames[k - 1])
obj.data.shape_keys.key_blocks[sframe].value = 1.0
obj.data.shape_keys.key_blocks[sframe].keyframe_insert(data_path='value', frame=frames[k])
if k < len(frames) - 1:
obj.data.shape_keys.key_blocks[sframe].value = 0.0
obj.data.shape_keys.key_blocks[sframe].keyframe_insert(data_path='value', frame=frames[k + 1])

######################################

#Flag if a new object as been created...
Expand Down
138 changes: 133 additions & 5 deletions operators/io_import_shp.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
# -*- coding:utf-8 -*-
import os, sys, time
import os, sys, time, glob, re
import bpy
from bpy.props import StringProperty, BoolProperty, EnumProperty, IntProperty
from bpy.types import Operator
Expand Down Expand Up @@ -104,7 +104,7 @@ class IMPORTGIS_OT_shapefile_props_dialog(Operator):
def check(self, context):
return True

def listFields(self, context):
def listFields(self, context=None):
fieldsItems = []
try:
shp = shpReader(self.filepath)
Expand Down Expand Up @@ -169,6 +169,12 @@ def listObjects(self, context):
description = "Choose field",
items = listFields )

#Animate across multiple elevation values in different fields
animateElevationFields: BoolProperty(
name="Animate elevation fields",
description="Animate elevation from multiple fields",
default=False )

#Extrusion field
useFieldExtrude: BoolProperty(
name="Extrusion from field",
Expand All @@ -193,6 +199,12 @@ def listObjects(self, context):
description="Warning : can be very slow with lot of features",
default=False )

#Animate across shapefiles
animate: BoolProperty(
name="Animate multiple shapefiles",
description="Animate across other shapefiles in the same folder",
default=False )

#Name objects from field
useFieldName: BoolProperty(
name="Object name from field",
Expand All @@ -215,6 +227,7 @@ def draw(self, context):
#layout.prop(self, 'useFieldElev')
if self.vertsElevSource == 'FIELD':
layout.prop(self, 'fieldElevName')
layout.prop(self, 'animateElevationFields')
elif self.vertsElevSource == 'OBJ':
layout.prop(self, 'objElevLst')
#
Expand All @@ -227,6 +240,7 @@ def draw(self, context):
if self.separateObjects:
layout.prop(self, 'useFieldName')
else:
layout.prop(self, 'animate')
self.useFieldName = False
if self.separateObjects and self.useFieldName:
layout.prop(self, 'fieldObjName')
Expand Down Expand Up @@ -285,9 +299,123 @@ def execute(self, context):
shpCRS = self.shpCRS

try:
bpy.ops.importgis.shapefile('INVOKE_DEFAULT', filepath=self.filepath, shpCRS=shpCRS, elevSource=self.vertsElevSource,
fieldElevName=elevField, objElevName=objElevName, fieldExtrudeName=extrudField, fieldObjName=nameField,
extrusionAxis=self.extrusionAxis, separateObjects=self.separateObjects)
if self.animate:
basedir = os.path.dirname(self.filepath)
files = glob.glob(os.path.join(basedir, "*.shp"))
files.sort()
objects = []
frame = 0
for f in files:
bpy.ops.importgis.shapefile('INVOKE_DEFAULT', filepath=f, shpCRS=shpCRS, elevSource=self.vertsElevSource,
fieldElevName=elevField, objElevName=objElevName, fieldExtrudeName=extrudField, fieldObjName=nameField,
extrusionAxis=self.extrusionAxis, separateObjects=self.separateObjects)
obj = bpy.context.selected_objects[0]
try:
frame = int(re.search(r'\d+', obj.name).group())
except:
frame += 1
obj["frame"] = frame

vertex_count = len(obj.data.vertices)
area = sum(p.area for p in obj.data.polygons)
obj["area"] = area
log.info("{} has {} vertices and an area of {}".format(obj.name, vertex_count, area))
triangulate = obj.modifiers.new("TRIANGULATE", "TRIANGULATE")
bpy.ops.object.modifier_apply(modifier="TRIANGULATE")

# Subsurf to smooth edges
n_cuts_required = int(round(math.log(500.0 / vertex_count, 2)))
if n_cuts_required >= 1:
print(n_cuts_required)
subsurf = obj.modifiers.new("SUBSURF", "SUBSURF")
subsurf.levels = n_cuts_required
bpy.ops.object.modifier_apply(modifier="SUBSURF")
objects.append(obj)
log.info("After triangulation / subsurf {} has {} vertices".format(obj.name, len(obj.data.vertices)))

# Strategy here is to take the largest object, then progressively shrinkwrap it onto each smaller object in turn
# To do this, first sort objects by area descending
objects.sort(key=lambda x: x["area"], reverse=True)
base_obj = objects[0] # This is the largest object / the object that will hold the shape keys
print("base_obj is " + base_obj.name)
base_obj.name = base_obj.name.replace(str(base_obj["frame"]), "")

bpy.ops.object.duplicate()
duplicated_base = bpy.context.view_layer.objects.active
# Use a duplicated object as a reference for iterative shrinkwrapping

base_obj.shape_key_add(name=str(base_obj["frame"]))

for obj in objects:
if obj == base_obj:
continue
shrinkwrap = duplicated_base.modifiers.new("SHRINKWRAP", "SHRINKWRAP")
shrinkwrap.target = obj
#shrinkwrap.wrap_method = "NEAREST_VERTEX"
bpy.ops.object.modifier_apply(modifier="SHRINKWRAP")
# Copy vertex data from shrinkwrapped duplicate as a shape key
base_obj.shape_key_add(name=str(obj["frame"]))
for j, vertex in enumerate(duplicated_base.data.vertices):
base_obj.data.shape_keys.key_blocks[str(obj["frame"])].data[j].co = vertex.co

bpy.data.objects.remove(duplicated_base, do_unlink = True)
frames = sorted(o["frame"] for o in objects)

for k, frame in enumerate(frames):
# Create keyframes
sframe = str(frame)
if k > 0:
base_obj.data.shape_keys.key_blocks[sframe].value = 0.0
base_obj.data.shape_keys.key_blocks[sframe].keyframe_insert(data_path='value', frame=frames[k - 1])
base_obj.data.shape_keys.key_blocks[sframe].value = 1.0
base_obj.data.shape_keys.key_blocks[sframe].keyframe_insert(data_path='value', frame=frames[k])
if k < len(frames) - 1:
base_obj.data.shape_keys.key_blocks[sframe].value = 0.0
base_obj.data.shape_keys.key_blocks[sframe].keyframe_insert(data_path='value', frame=frames[k + 1])

for obj in objects:
if obj != base_obj:
bpy.data.objects.remove(obj, do_unlink = True)

elif self.animateElevationFields:
base = elevField.rstrip('0123456789')
fields = [field[0] for field in self.listFields() if field[0].startswith(base)]
frames = [int(field.replace(base, "")) for field in fields]
print(fields)
# Load first elevation field
bpy.ops.importgis.shapefile('INVOKE_DEFAULT', filepath=self.filepath, shpCRS=shpCRS, elevSource=self.vertsElevSource,
fieldElevName=fields[0], objElevName=objElevName, fieldExtrudeName=extrudField, fieldObjName=nameField,
extrusionAxis=self.extrusionAxis, separateObjects=self.separateObjects)
base_obj = bpy.context.selected_objects[0]
base_obj.shape_key_add(name=fields[0])

for field in fields[1:]:
print(field)
bpy.ops.importgis.shapefile('INVOKE_DEFAULT', filepath=self.filepath, shpCRS=shpCRS, elevSource=self.vertsElevSource,
fieldElevName=field, objElevName=objElevName, fieldExtrudeName=extrudField, fieldObjName=nameField,
extrusionAxis=self.extrusionAxis, separateObjects=self.separateObjects)
obj = bpy.context.selected_objects[0]
base_obj.shape_key_add(name=field)
for j, vertex in enumerate(obj.data.vertices):
base_obj.data.shape_keys.key_blocks[field].data[j].co = vertex.co
bpy.data.objects.remove(obj, do_unlink = True)

# Create keyframes
for k, field in enumerate(fields):
frame = int(field.replace(base, ""))
if k > 0:
base_obj.data.shape_keys.key_blocks[field].value = 0.0
base_obj.data.shape_keys.key_blocks[field].keyframe_insert(data_path='value', frame=frames[k - 1])
base_obj.data.shape_keys.key_blocks[field].value = 1.0
base_obj.data.shape_keys.key_blocks[field].keyframe_insert(data_path='value', frame=frames[k])
if k < len(frames) - 1:
base_obj.data.shape_keys.key_blocks[field].value = 0.0
base_obj.data.shape_keys.key_blocks[field].keyframe_insert(data_path='value', frame=frames[k + 1])

else:
bpy.ops.importgis.shapefile('INVOKE_DEFAULT', filepath=self.filepath, shpCRS=shpCRS, elevSource=self.vertsElevSource,
fieldElevName=elevField, objElevName=objElevName, fieldExtrudeName=extrudField, fieldObjName=nameField,
extrusionAxis=self.extrusionAxis, separateObjects=self.separateObjects)
except Exception as e:
log.error('Shapefile import fails', exc_info=True)
self.report({'ERROR'}, 'Shapefile import fails, check logs.')
Expand Down