├── GES_Panel_1_2.py ├── LICENSE ├── README.md └── Version 1.2 update /GES_Panel_1_2.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2021 imagiscope 2 | 3 | # Permission is hereby granted, free of charge, to any person obtaining a copy 4 | # of this software and associated documentation files (the "Software"), to deal 5 | # in the Software without restriction, including without limitation the rights 6 | # to use the Software, and to permit persons to whom the Software is 7 | # furnished to do so, subject to the following conditions: 8 | 9 | # 1. Not for resale (use of software tools for revenue generation excluded). 10 | # 2. Credit to: "https://www.youtube.com/c/ImagiscopeTech" 11 | # 3. Notification of commercial use to author. 12 | 13 | # The above copyright notice and this permission notice shall be included in all 14 | # copies or substantial portions of the Software. 15 | 16 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | # SOFTWARE. 23 | 24 | bl_info = { 25 | "name": "Earth Studio", 26 | "author": "Rob Jolly - Imagiscope", 27 | "description": "Earth Studio Tools Addon", 28 | "blender": (2, 80, 0), 29 | "version": (1, 2, 2), 30 | "location": "View3D", 31 | "warning": "", 32 | "category": "Import-Export" 33 | } 34 | 35 | import bpy, json, mathutils, math, bmesh 36 | from mathutils import * 37 | from bpy.props import EnumProperty 38 | from xml.dom.minidom import parse 39 | from xml.dom.minidom import Node 40 | from xml.etree import cElementTree as ElementTree 41 | 42 | import numpy 43 | 44 | 45 | class GES_OT_Path(bpy.types.PropertyGroup): 46 | p_data: bpy.props.StringProperty(name="0000",subtype='FILE_PATH',default=r"") 47 | p_movie: bpy.props.StringProperty(name="data",subtype='FILE_PATH',default=r"") 48 | p_kml: bpy.props.StringProperty(name="kml",subtype='FILE_PATH',default=r"") 49 | p_refdata: bpy.props.StringProperty(name="refdata",subtype='FILE_PATH',default=r"") 50 | 51 | p_objexpfolder: bpy.props.StringProperty(name="expdata",subtype='DIR_PATH',default=r"//") 52 | p_objexp: bpy.props.StringProperty(name="expdata",subtype='FILE_NAME',default=r"ObjectKML") 53 | 54 | v_curve: bpy.props.EnumProperty(name="Curve",items=[('NURBS',"Nurbs",""),('POLY',"Poly","")]) 55 | 56 | def trackitems(self,context): 57 | t_trks = [] 58 | objects = bpy.data.objects["_GES_WORLD"].children 59 | for obj in objects: 60 | 61 | if obj.type == "MESH": 62 | if obj.data.name[0:4] == "Plan": #mod for some international languages 63 | t_trks.append(( obj.name, obj.name,"")) 64 | 65 | return t_trks 66 | v_snapto: bpy.props.EnumProperty( 67 | name = "Snap to", 68 | description = "Snap to TrackPoint in _GES_WORLD", 69 | items = trackitems 70 | ) 71 | 72 | v_terrain: bpy.props.BoolProperty(name="Follow Terrain",description="Align the KML route with supplied JSON TrackPoints.", default = True) 73 | 74 | v_elevation: bpy.props.IntProperty(name="Add Elevation (m)", default=0, min=-100, max=10000, 75 | description="Add elevation to Route (0 is none, 1000 = 1km, 100000 = 10km)" ) 76 | 77 | v_reduce: bpy.props.IntProperty(name="Point Reduction", default=2, min=0, max=100, 78 | description="Reduce KML points based on clustering (1 is less reduction, 100 is more reduction)" ) 79 | v_prox: bpy.props.IntProperty(name="Match Proximity (m)", default=1, min=1, max=1000, 80 | description="Set altitude based on meters (approx) to trackpoint (1 is closer, 100 more forgiving)") 81 | 82 | v_bevel: bpy.props.FloatProperty(name="Bevel Depth", default=0, min=0, max=5, 83 | description="Starting Route Bevel (0 = none)" ) 84 | 85 | v_objlinecolor: bpy.props.FloatVectorProperty(name="Line Color", subtype='COLOR', default=[0.0,1.0,0.0]) 86 | v_objfillcolor: bpy.props.FloatVectorProperty(name="Fill Color", subtype='COLOR', default=[1.0,1.0,1.0]) 87 | 88 | v_objlinewidth: bpy.props.IntProperty(name="Line Width", default=0, min=0, max=50, 89 | description="Width of line (0 = none)" ) 90 | 91 | 92 | v_objfillopacity: bpy.props.IntProperty(name="Fill Opacity", default=100, min=1, max=100) 93 | 94 | def nontrackitems(self,context): 95 | t_trks = [] 96 | objects = bpy.context.scene.objects 97 | for obj in objects: 98 | nonges = False # Only display root obects - no cameras, no lights, no GES items 99 | if (obj.parent) == None: 100 | nonges = True 101 | if (obj.type) == 'LIGHT' or (obj.type) == 'CAMERA': 102 | nonges = False 103 | if obj.name[0:5] != "_GES_" and nonges == True and obj.name[0:7] != "Marker_" : 104 | t_trks.append(( obj.name, obj.name,"")) 105 | 106 | return t_trks 107 | v_mtemplate: bpy.props.EnumProperty( 108 | name = "Template", 109 | description = "Marker Template", 110 | items = nontrackitems 111 | ) 112 | v_mlookat: bpy.props.BoolProperty(name="Face to Camera",description="Align the Marker to the Camera.", default = True) 113 | 114 | 115 | # Earth Studio import panel 116 | class GES_PT_ImportPanel(bpy.types.Panel): 117 | bl_label = "Earth Studio Import" 118 | bl_idname = "GES_PT_ImportPanel" 119 | bl_space_type = 'VIEW_3D' 120 | bl_region_type = 'UI' 121 | bl_category = 'Earth Studio' 122 | 123 | def draw(self,context): 124 | layout = self.layout 125 | row = layout.row() 126 | row.label(text="Footage (mp4 or first jpeg):") 127 | row = layout.row() 128 | row.prop(bpy.context.scene.GES_OT_Path, "p_movie", text="",icon="IMAGE_DATA") 129 | row = layout.row() 130 | row.label(text="Earth Studio JSON File:") 131 | row = layout.row() 132 | row.prop(bpy.context.scene.GES_OT_Path, "p_data", text="",icon="VIEW_CAMERA") 133 | row = layout.row() 134 | fa = bpy.context.scene.GES_OT_Path.p_movie 135 | fb = bpy.context.scene.GES_OT_Path.p_data 136 | if fa != '' and fb != '': # ensure both selections have 'text' (simple validation) 137 | row.operator("scene.pre_ges", text="Import Earth Studio" ) 138 | if fa == '' or fb == '': 139 | row.operator("scene.is_void", text="Select Files" , icon="LOCKED") 140 | 141 | 142 | # Object to KML panel 143 | class GES_PT_ObjectKMLPanel(bpy.types.Panel): 144 | bl_label = "Export Object as KML" 145 | bl_idname = "GES_PT_ObjectKMLPanel" 146 | bl_space_type = 'VIEW_3D' 147 | bl_region_type = 'UI' 148 | bl_category = 'Earth Studio' 149 | bl_options = {'DEFAULT_CLOSED'} 150 | 151 | def draw(self,context): 152 | objects = bpy.context.scene.objects 153 | hasGES = 0 154 | for obj in objects: 155 | 156 | if obj.name == "_GES_WORLD": 157 | hasGES=1 158 | 159 | if hasGES == 1: 160 | selobj = bpy.context.active_object 161 | 162 | if selobj: 163 | 164 | if selobj.type == "MESH" or selobj.type == "CURVE": 165 | layout = self.layout 166 | 167 | row = layout.row() 168 | 169 | row.label(text="Selected: " + selobj.name) 170 | if selobj.type == "CURVE": 171 | row = layout.row() 172 | row.label(text="- Curve Optimized") 173 | row = layout.box() 174 | row.prop(bpy.context.scene.GES_OT_Path, "v_objfillcolor") 175 | row.prop(bpy.context.scene.GES_OT_Path, "v_objfillopacity") 176 | row = layout.box() 177 | row.prop(bpy.context.scene.GES_OT_Path, "v_objlinecolor") 178 | row.prop(bpy.context.scene.GES_OT_Path, "v_objlinewidth") 179 | row = layout.row() 180 | row.label(text="Destination Folder:") 181 | row = layout.row() 182 | row.prop(bpy.context.scene.GES_OT_Path, "p_objexpfolder", text="") 183 | row = layout.row() 184 | row.label(text="Filename:" ) 185 | row = layout.row() 186 | row.prop(bpy.context.scene.GES_OT_Path, "p_objexp", text="") 187 | row = layout.row() 188 | row.operator("scene.pre_objkml", text="Export Object as KML" ).action = "pri" 189 | else: 190 | layout = self.layout 191 | 192 | row = layout.row() 193 | row.label(text="Invalid: Mesh or Curve Only") 194 | row = layout.row() 195 | row.label(text="Try Converting to Mesh") 196 | else: 197 | layout = self.layout 198 | row = layout.row() 199 | row.label(text="Select Object", icon="LOCKED") 200 | row = layout.row() 201 | 202 | if hasGES == 0: # 'disable' section if there is no imported project 203 | layout = self.layout 204 | row = layout.row() 205 | row.label(text="Import Earth Studio first", icon="LOCKED") 206 | row = layout.row() 207 | 208 | 209 | # KML import panel 210 | class GES_PT_KMLPanel(bpy.types.Panel): 211 | bl_label = "Import KML Route" # Import/Export" 212 | bl_idname = "GES_PT_KMLPanel" 213 | bl_space_type = 'VIEW_3D' 214 | bl_region_type = 'UI' 215 | bl_category = 'Earth Studio' 216 | bl_options = {'DEFAULT_CLOSED'} 217 | 218 | def draw(self,context): 219 | objects = bpy.context.scene.objects 220 | hasGES = 0 221 | for obj in objects: 222 | 223 | if obj.name == "_GES_WORLD": 224 | hasGES=1 225 | 226 | if hasGES == 1: # enabled 227 | 228 | context.area.tag_redraw() 229 | layout = self.layout 230 | row = layout.row() 231 | row.label(text="KML File (route):") 232 | row = layout.row() 233 | row.prop(bpy.context.scene.GES_OT_Path, "p_kml", text="", icon="WORLD") 234 | row = layout.row() 235 | row.prop(bpy.context.scene.GES_OT_Path, "v_snapto") 236 | row = layout.row() 237 | row.prop(bpy.context.scene.GES_OT_Path, "v_curve") 238 | row = layout.row() 239 | row.prop(bpy.context.scene.GES_OT_Path, "v_bevel") 240 | row = layout.row() 241 | row.prop(bpy.context.scene.GES_OT_Path, "v_elevation") 242 | row = layout.row() 243 | row.prop(bpy.context.scene.GES_OT_Path, "v_terrain") 244 | if str(bpy.context.scene.GES_OT_Path.v_terrain) == "True": 245 | row = layout.box() 246 | row.label(text="Reference JSON File:") 247 | row.prop(bpy.context.scene.GES_OT_Path, "p_refdata", text="", icon="LIBRARY_DATA_DIRECT") 248 | row.prop(bpy.context.scene.GES_OT_Path, "v_reduce") 249 | row.prop(bpy.context.scene.GES_OT_Path, "v_prox") 250 | row = layout.row() 251 | 252 | fa = bpy.context.scene.GES_OT_Path.p_kml 253 | fb = bpy.context.scene.GES_OT_Path.p_refdata 254 | if fa != '': #(simple validation) 255 | row.operator("scene.pre_kml", text="Import KML Route" ).action = "pri" 256 | 257 | if fa == '': 258 | row.operator("scene.is_void", text="Select Files" , icon="LOCKED") 259 | if hasGES == 0: # 'disable' section if there is no imported project 260 | layout = self.layout 261 | row = layout.row() 262 | row.label(text="Import Earth Studio first", icon="LOCKED") 263 | row = layout.row() 264 | 265 | # Marker Panel 266 | class GES_PT_MarkerPanel(bpy.types.Panel): 267 | bl_label = "Trackpoint Marker Tool" 268 | bl_idname = "GES_PT_MarkerPanel" 269 | bl_space_type = 'VIEW_3D' 270 | bl_region_type = 'UI' 271 | bl_category = 'Earth Studio' 272 | bl_options = {'DEFAULT_CLOSED'} 273 | 274 | def draw(self,context): 275 | objects = bpy.context.scene.objects 276 | hasGES = 0 277 | for obj in objects: 278 | 279 | if obj.name == "_GES_WORLD": 280 | hasGES=1 281 | 282 | 283 | if hasGES == 1: # enabled 284 | 285 | context.area.tag_redraw() 286 | layout = self.layout 287 | row = layout.row() 288 | row.label(text="Add Marker for each Trackpoint") 289 | row = layout.row() 290 | row.label(text="1. Create Template Marker") 291 | row = layout.row() 292 | row.label(text=" - Parent all objects to object/empty") 293 | row = layout.row() 294 | row.label(text=" - Text Object will use Trackpoint name") 295 | row = layout.row() 296 | row.label(text=" - Origin of Parent is rotation point") 297 | row = layout.row() 298 | row.label(text="2. Select Template Marker") 299 | row = layout.row() 300 | row.label(text="3. Use 'Face..' to always point to Camera") 301 | row = layout.row() 302 | row.prop(bpy.context.scene.GES_OT_Path, "v_mtemplate") 303 | 304 | row = layout.row() 305 | row.prop(bpy.context.scene.GES_OT_Path, "v_mlookat") 306 | 307 | row = layout.row() 308 | 309 | 310 | row.operator("scene.pre_marker", text="Create Markers" ) 311 | 312 | 313 | if hasGES == 0: # 'disable' section if there is no imported project 314 | layout = self.layout 315 | row = layout.row() 316 | row.label(text="Import Earth Studio first", icon="LOCKED") 317 | row = layout.row() 318 | 319 | # Help Panel 320 | class GES_PT_InfoPanel(bpy.types.Panel): 321 | bl_label = "Help" 322 | bl_idname = "GES_PT_InfoPanel" 323 | bl_space_type = 'VIEW_3D' 324 | bl_region_type = 'UI' 325 | bl_category = 'Earth Studio' 326 | bl_options = {'DEFAULT_CLOSED'} 327 | 328 | def draw(self,context): 329 | layout = self.layout 330 | 331 | row = layout.row() 332 | row.label(text="Tutorials") 333 | row = layout.row() 334 | op = row.operator('wm.url_open', text="View on YouTube") 335 | op.url = "https://www.youtube.com/c/ImagiscopeTech" 336 | 337 | row = layout.row() 338 | 339 | # Void class - returns nothing 340 | class isvoid(bpy.types.Operator): 341 | bl_idname = "scene.is_void" 342 | bl_label = "GES is void" 343 | 344 | def execute(self, context): 345 | return {'FINISHED'} 346 | 347 | # check files 348 | class preobjKML(bpy.types.Operator): 349 | bl_idname = "scene.pre_objkml" 350 | bl_label = "GES PRE KML" 351 | 352 | action: EnumProperty(items=[('pri','pri','pri'),('sec','sec','sec')]) 353 | 354 | def execute(self, context): 355 | if self.action == "pri": 356 | objecttokml() 357 | 358 | return {'FINISHED'} 359 | 360 | 361 | # check files 362 | class preKML(bpy.types.Operator): 363 | bl_idname = "scene.pre_kml" 364 | bl_label = "GES PRE KML" 365 | 366 | action: EnumProperty(items=[('pri','pri','pri'),('sec','sec','sec')]) 367 | 368 | def execute(self, context): 369 | if self.action == "pri": 370 | importkml() 371 | 372 | return {'FINISHED'} 373 | 374 | 375 | class preGES(bpy.types.Operator): 376 | bl_idname = "scene.pre_ges" 377 | bl_label = "GES PRE GES" 378 | 379 | def execute(self, context): 380 | fa = bpy.context.scene.GES_OT_Path.p_movie 381 | fb = bpy.context.scene.GES_OT_Path.p_data 382 | if fa != '' and fb != '': 383 | importges() 384 | objects = bpy.context.scene.objects 385 | hasGES = 0 386 | for obj in objects: 387 | if obj.name == "_GES_WORLD": 388 | hasGES=1 389 | if hasGES == 1: 390 | t_trks = [] 391 | objects = bpy.data.objects["_GES_WORLD"].children 392 | for obj in objects: 393 | if obj.type == "MESH": 394 | if obj.data.name[0:4] == "Plan": 395 | t_trks.append(( obj.name, obj.name,"")) 396 | v_snapto = bpy.props.EnumProperty( 397 | name = "Snap to",description = "Snap to TrackPoint in _GES_WORLD",items = t_trks ) 398 | 399 | 400 | return {'FINISHED'} 401 | 402 | # check files 403 | class preMarker(bpy.types.Operator): 404 | bl_idname = "scene.pre_marker" 405 | bl_label = "GES PRE MARKER" 406 | 407 | 408 | def execute(self, context): 409 | makemarkers() 410 | return {'FINISHED'} 411 | 412 | # Info Popup 413 | def ShowMessageBox(message = "", title = "Information", icon = 'INFO'): 414 | def draw(self, context): 415 | self.layout.label(text = message) 416 | bpy.context.window_manager.popup_menu(draw, title = title, icon = icon) 417 | 418 | def importges(): 419 | 420 | cam = bpy.context.scene.camera 421 | if not cam: # add a camera if deleted 422 | 423 | cam_d = bpy.data.cameras.new(name='Camera') 424 | cam_o = bpy.data.objects.new('Camera', cam_d) 425 | bpy.data.collections['Collection'].objects.link(cam_o) 426 | cam = cam_o 427 | bpy.context.scene.camera = cam 428 | 429 | scene = bpy.context.scene 430 | 431 | # load JSON file for evaluation 432 | # Sample format: jfilename = "D:/Local/Project/Beach/beach/beach.json" 433 | jfilename = bpy.path.abspath(bpy.context.scene.GES_OT_Path.p_data) 434 | 435 | jfile = open(jfilename,'r') 436 | camdata = json.load(jfile) 437 | jfile.close 438 | hasTrack = False 439 | for cd in camdata: 440 | if cd == "trackPoints": 441 | hasTrack=True 442 | # check trackpoints 443 | if hasTrack == False: 444 | ShowMessageBox( "Ensure Earth Studio project has Trackpoints (min 1) and export JSON file with trackpoints.","Import Aborted - No Trackpoints Found","ERROR") 445 | else: 446 | 447 | # load the GES render files as background for camera 448 | # Sample format: ifiles = "D:/Local/Project/Beach/beach/footage/beach_0000.jpeg" 449 | ifiles = bpy.path.abspath(bpy.context.scene.GES_OT_Path.p_movie) 450 | 451 | img = bpy.data.movieclips.load(ifiles) 452 | cam.data.show_background_images = True 453 | cam.data.clip_end = 10000 454 | 455 | bg = cam.data.background_images.new() 456 | bg.clip = img 457 | bg.alpha = 1 458 | bg.source = "MOVIE_CLIP" 459 | 460 | # evaluate number of frames 461 | s_end = camdata["numFrames"] 462 | 463 | # set scene duration 464 | scene.frame_start = 1 465 | scene.frame_end = s_end 466 | scene.frame_set(1) 467 | 468 | # function for alignment scaling 469 | def scale_from_vector(v): 470 | mat = Matrix.Identity(4) 471 | for i in range(3): 472 | mat[i][i] = v[i] 473 | return mat 474 | 475 | # set coords for positioning data starting at center of Blender global coordinates 476 | psx = 0 477 | psy = 0 478 | psz = 0 479 | 480 | # load trackpoints 481 | for f in range (0,len(camdata["trackPoints"])): 482 | 483 | px = camdata["trackPoints"][f]["position"]["x"] 484 | py = camdata["trackPoints"][f]["position"]["y"] 485 | pz = camdata["trackPoints"][f]["position"]["z"] 486 | 487 | rlat = camdata["trackPoints"][f]["coordinate"]["position"]["attributes"][0]["value"]["relative"] 488 | rlng = camdata["trackPoints"][f]["coordinate"]["position"]["attributes"][1]["value"]["relative"] 489 | 490 | if f==0: 491 | psx = px 492 | psy = py 493 | psz = pz 494 | 495 | rlat = 360 * (rlat) - 180 496 | rlng = (89.9999*2) * (rlng ) - 89.9999 497 | 498 | bpy.ops.mesh.primitive_plane_add() 499 | trk = bpy.context.selected_objects[0] 500 | trk.name = str(f + 1) + ". " + camdata["trackPoints"][f]["name"] 501 | 502 | trk.location.x = (px-psx) / 100 503 | trk.location.y = (py-psy) / 100 504 | trk.location.z = (pz-psz) / 100 505 | 506 | trk.rotation_euler[1] = math.radians(90-rlng) 507 | trk.rotation_euler[2] = math.radians(rlat) 508 | trk.scale = (0.1,0.1,0.1) 509 | 510 | calt = camdata["trackPoints"][f]["coordinate"]["position"]["attributes"][2]["value"]["relative"] 511 | trk['X'] = px 512 | trk['Y'] = py 513 | trk['Z'] = pz 514 | trk['LAT'] = rlng # real lat - mislabeled 515 | trk['LNG'] = rlat # real lng - mislabeled 516 | trk['ALT'] = 65117481 * (calt) + 1 517 | 518 | if f==0: 519 | # create parent object - parent used to align position on earth with Blender global coordinates 520 | bpy.ops.object.empty_add(type='SINGLE_ARROW', location=(0,0,0)) 521 | ges_parent = bpy.context.selected_objects[0] 522 | ges_parent.name = "_GES_WORLD" 523 | 524 | # align parent perpendicular to first track point 525 | loc_src, rot_src, scale_src = trk.matrix_world.decompose() 526 | loc_dst, rot_dst, scale_dst = ges_parent.matrix_world.decompose() 527 | 528 | axis = Vector((0.0, 0.0, 1.0)) 529 | z1 = rot_src @ axis 530 | z2 = rot_dst @ axis 531 | q = z2.rotation_difference(z1) 532 | 533 | ges_parent.matrix_world = ( 534 | Matrix.Translation(loc_dst) @ 535 | (q @ rot_dst).to_matrix().to_4x4() @ 536 | scale_from_vector(scale_dst) 537 | ) 538 | 539 | # change x,y to negative values of x,y 540 | ges_parent.rotation_euler[0] = -ges_parent.rotation_euler[0] 541 | ges_parent.rotation_euler[1] = -ges_parent.rotation_euler[1] 542 | 543 | # move trackpoint to GES parent 544 | trk.parent = ges_parent 545 | 546 | 547 | # Camera Information 548 | cam.delta_rotation_euler.y = 180 * math.pi / 180 549 | 550 | for f in range (0,s_end + 1): 551 | px = camdata["cameraFrames"][f]["position"]["x"] 552 | py = camdata["cameraFrames"][f]["position"]["y"] 553 | pz = camdata["cameraFrames"][f]["position"]["z"] 554 | 555 | 556 | rx = float(camdata["cameraFrames"][f]["rotation"]["x"]) 557 | ry = camdata["cameraFrames"][f]["rotation"]["y"] 558 | rz = camdata["cameraFrames"][f]["rotation"]["z"] 559 | 560 | # position set in relation to first frame - scale to 1/100 561 | cam.location.x = (px-psx) / 100 562 | cam.location.y = (py-psy) / 100 563 | cam.location.z = (pz-psz) / 100 564 | 565 | eul = mathutils.Euler((0.0, 0.0, 0.0), 'XYZ') 566 | 567 | eul.rotate_axis('X', math.radians(-rx)) 568 | eul.rotate_axis('Y', math.radians(ry )) 569 | eul.rotate_axis('Z', math.radians(-rz+180)) 570 | 571 | cam.rotation_euler = eul 572 | 573 | cam.keyframe_insert(data_path="location", index=-1, frame=f + 1) 574 | cam.keyframe_insert(data_path="rotation_euler", index=-1, frame=f + 1) 575 | 576 | 577 | # camera "lens" based on 20 degree Filed of View (default value) 578 | cam.data.sensor_width = 35 579 | cam.data.type = 'PERSP' 580 | cam.data.lens_unit = 'FOV' 581 | cam.data.angle = math.radians(34.8) 582 | 583 | # move camera to GES parent 584 | cam.parent = ges_parent 585 | 586 | bpy.context.scene.frame_current = 1 587 | area = next(area for area in bpy.context.screen.areas if area.type == 'VIEW_3D') 588 | area.spaces[0].region_3d.view_perspective = 'CAMERA' 589 | 590 | def importkml(): 591 | earth = 6371010.1 #earth radius, in meters 592 | add_elev = float(bpy.context.scene.GES_OT_Path.v_elevation) 593 | sn = bpy.data.objects[bpy.context.scene.GES_OT_Path.v_snapto] 594 | 595 | tralt = sn["ALT"] 596 | 597 | objects = bpy.data.objects["_GES_WORLD"].children 598 | v_zerotrack = "" #initial center plane 599 | for obj in objects: 600 | if obj.type == "MESH": 601 | if obj.data.name[0:5] == "Plane": 602 | if v_zerotrack == "": 603 | v_zerotrack = obj.name 604 | 605 | # function for alignment scaling 606 | def scale_from_vector(v): 607 | mat = Matrix.Identity(4) 608 | for i in range(3): 609 | mat[i][i] = v[i] 610 | return mat 611 | 612 | # function to measure distance between two coordinates 613 | def measure(lat1, lon1, lat2, lon2): 614 | 615 | dLat = lat2 * math.pi / 180 - lat1 * math.pi / 180 616 | dLon = lon2 * math.pi / 180 - lon1 * math.pi / 180 617 | a = math.sin(dLat/2) * math.sin(dLat/2) + math.cos(lat1 * math.pi / 180) * math.cos(lat2 * math.pi / 180) * math.sin(dLon/2) * math.sin(dLon/2) 618 | c = 2 * math.atan2(math.sqrt(a), math.sqrt(1-a)) 619 | d = earth * c 620 | return d 621 | 622 | 623 | # make a new curve 624 | crv = bpy.data.curves.new('crv', 'CURVE') 625 | crv.dimensions = '3D' 626 | 627 | # make a new spline in that curve 628 | spline = crv.splines.new(type=bpy.context.scene.GES_OT_Path.v_curve) 629 | spline.resolution_u = 6 630 | spline.order_u = 12 631 | 632 | 633 | # load kml file for evaluation 634 | xfilename = bpy.path.abspath(bpy.context.scene.GES_OT_Path.p_kml) 635 | domData = parse(xfilename) 636 | coor = domData.getElementsByTagName("coordinates") 637 | 638 | pl = "" 639 | if coor.length != 0: 640 | for i in range (0,coor.length ): 641 | if coor[i].parentNode.nodeName != "Point": 642 | pl = coor[0].firstChild.nodeValue.strip() 643 | pl = pl.replace("\n"," ") 644 | while pl.find(" ") != -1: 645 | pl = pl.replace (" "," ") 646 | #print ("coord found") 647 | if coor.length == 0 or pl == "": # try gx:coord method 648 | gxcoor = domData.getElementsByTagName("gx:coord") 649 | print ("xx") 650 | if gxcoor.length != 0: 651 | for i in range (0,gxcoor.length): #format like coordinates 652 | if i != 0: 653 | pl+= " " 654 | pl += str(gxcoor[i].firstChild.nodeValue.strip()).replace(" ",",") 655 | print (pl) 656 | elif gxcoor.length == 0: 657 | return 658 | #print (pl) 659 | 660 | pt = pl.split(' ') 661 | #if add_elev != 0: 662 | if str(bpy.context.scene.GES_OT_Path.v_terrain) != 'True': # replace anchor value with trackpoint alt 663 | tt = pt[0].split(",") 664 | pt[0] = str(tt[0]) + "," + str(tt[1]) + "," + str(tralt) 665 | pl = pt[0] + " " + pl # insert start point twice (anchor) 666 | pt = pl.split(' ') 667 | 668 | # add placeholder end coordinate 669 | pt.append (str(0) + "," + str(0) + "," + str(0) ) 670 | 671 | # load JSON file for evaluation 672 | # Sample format: jfilename = "D:/Local/Project/Beach/beach/beach.json" 673 | 674 | if str(bpy.context.scene.GES_OT_Path.v_terrain) == 'True': 675 | jfilename = bpy.path.abspath(bpy.context.scene.GES_OT_Path.p_refdata) 676 | 677 | jfile = open(jfilename,'r') 678 | camdata = json.load(jfile) 679 | jfile.close 680 | 681 | 682 | lpt = 0 683 | 684 | if str(bpy.context.scene.GES_OT_Path.v_terrain) == 'True': 685 | # calculate altitude based on track points, incline/decline from A to B 686 | prox = bpy.context.scene.GES_OT_Path.v_prox /10000 # set altitude base on "closeness" to trackpoint - default 0.001 (0.0001 is closer, 0.01 more forgiving) 687 | for i in range (0,len(pt)): 688 | plat = float(pt[i].split(',')[0]) 689 | plng = float(pt[i].split(',')[1]) 690 | fnd = 0 691 | for z in range (0,len(camdata["trackPoints"])): 692 | if fnd == 0: 693 | tlat =camdata["trackPoints"][z]["coordinate"]["position"]["attributes"][0]["value"]["relative"] 694 | tlng =camdata["trackPoints"][z]["coordinate"]["position"]["attributes"][1]["value"]["relative"] 695 | talt =camdata["trackPoints"][z]["coordinate"]["position"]["attributes"][2]["value"]["relative"] 696 | 697 | tname = camdata["trackPoints"][z]["name"] 698 | xlat = 360 * (tlat) - 180 699 | xlng = (89.9999*2) * (tlng ) - 89.9999 700 | xalt = 65117481 * (talt) + 1 # base elevation 701 | 702 | if i == 0: 703 | xalt = xalt - add_elev 704 | pt[i] = str(plat) + "," + str(plng) + "," + str(xalt) 705 | if (xlat - (plat) < prox and xlat - (plat) > - prox) and (xlng - (plng) < prox and xlng - (plng) > -prox): 706 | pt[i] = str(plat) + "," + str(plng) + "," + str(xalt) 707 | fnd = 1 708 | 709 | 710 | if (fnd == 1 and i > 0) or (i == len(pt)-1 and fnd == 0): 711 | pplat = float(pt[lpt].split(',')[0]) 712 | pplng = float(pt[lpt].split(',')[1]) 713 | ppalt = float(pt[lpt].split(',')[2]) 714 | for d in range (lpt + 1,i ): 715 | if i == len(pt)-1 and fnd == 0: 716 | xlat = pplat 717 | xlng = pplng 718 | xalt = ppalt 719 | 720 | dlat = float(pt[d].split(',')[0]) 721 | dlng = float(pt[d].split(',')[1]) 722 | d1 = measure(pplat,pplng,dlat,dlng) # distance between last alt and this 723 | d2 = measure(dlat,dlng,xlat,xlng) # distance between trackpoint alt and this 724 | dratio = d1/(d1+d2) 725 | newalt = ppalt + ((xalt - ppalt) * dratio) 726 | 727 | pt[d] = str(dlat) + "," + str(dlng) + "," + str(newalt) 728 | 729 | lpt = i 730 | 731 | # reset start point with elevation change 732 | #stln = pt[0].split(",") 733 | #pt[0] = str(stln[0]) + "," + str(stln[1]) + "," + str(float(stln[2]) + add_elev) 734 | 735 | 736 | # convert lat/lon to points in 3D space on globe 737 | prevx = 0 738 | prevy = 0 739 | redval = bpy.context.scene.GES_OT_Path.v_reduce # reduce KML points based on closeness - default 10 (1 is closer (less reduction), 100 further away (more reduction)) 740 | pn = [] 741 | for i in range (0,len(pt)-1): 742 | lon = float(pt[i].split(',')[0]) 743 | lat = float(pt[i].split(',')[1]) 744 | alt = float(pt[i].split(',')[2]) 745 | 746 | if str(bpy.context.scene.GES_OT_Path.v_terrain) != 'True' and i==0: 747 | alt = alt - (add_elev) 748 | phi = (90 - lat) * (math.pi / 180); 749 | theta = (lon + 180) * (math.pi / 180); 750 | ox = -((earth + alt) * math.sin(phi) * math.cos(theta)); 751 | oy = -((earth + alt) * math.sin(phi) * math.sin(theta)); 752 | oz = ((earth + alt) * math.cos(phi)) 753 | 754 | if (prevx + redval < ox or prevx - redval > ox) and (prevy + redval < oy or prevy - redval > oy) or i<2 or redval==0: 755 | pn.append( pt[i] + ',' + str(ox) + ',' + str(oy) + ',' + str(oz) ) 756 | prevx = ox 757 | prevy = oy 758 | 759 | # set coordinates to spline 760 | flat = float(pn[0].split(',')[0]) 761 | flng = float(pn[0].split(',')[1]) 762 | psx = float(pn[0].split(',')[3]) 763 | psy = float(pn[0].split(',')[4]) 764 | psz = float(pn[0].split(',')[5]) 765 | 766 | spline.points.add(len(pn)-1) 767 | 768 | for p, new_co in zip(spline.points, pn): 769 | 770 | px = float(new_co.split(',')[3]) 771 | py = float(new_co.split(',')[4]) 772 | pz = float(new_co.split(',')[5]) 773 | p.co = (float((px - psx) / 100), float((py -psy) / 100), float((pz - psz) / 100), 1.0) 774 | 775 | #if add_elev != 0: 776 | for i in range (1,len(pn)): 777 | spline.points[i-1].co = spline.points[i].co 778 | 779 | # create curve object 780 | obj = bpy.data.objects.new('RoutePath', crv) 781 | 782 | 783 | 784 | # align path to surface of the globe 785 | print (v_zerotrack) 786 | obj.rotation_euler[1] = bpy.data.objects[v_zerotrack].rotation_euler[1] #math.radians(90-flng) 787 | obj.rotation_euler[2] = bpy.data.objects[v_zerotrack].rotation_euler[2] #math.radians(flat) 788 | 789 | 790 | bpy.data.collections['Collection'].objects.link(obj) 791 | 792 | #re-align to global system 793 | bpy.ops.object.empty_add(type='SINGLE_ARROW', location=(0,0,0)) # create empty container 794 | ges_path = bpy.context.selected_objects[0] 795 | ges_path.name = "_GES_PATH" 796 | 797 | loc_src, rot_src, scale_src = obj.matrix_world.decompose() 798 | loc_dst, rot_dst, scale_dst = ges_path.matrix_world.decompose() 799 | 800 | axis = Vector((0.0, 0.0, 1.0)) 801 | z1 = rot_src @ axis 802 | z2 = rot_dst @ axis 803 | q = z2.rotation_difference(z1) 804 | 805 | # set rotation based on object matrix 806 | ges_path.matrix_world = ( 807 | Matrix.Translation(loc_dst) @ 808 | (q @ rot_dst).to_matrix().to_4x4() @ 809 | scale_from_vector(scale_dst) 810 | ) 811 | 812 | # change x,y to negative values of x,y 813 | ges_path.rotation_euler[0] = -ges_path.rotation_euler[0] 814 | ges_path.rotation_euler[1] = -ges_path.rotation_euler[1] 815 | 816 | # creates anchor object - used to ensure path remains at height 817 | bpy.ops.object.empty_add(type='SINGLE_ARROW', location=(0,0,0)) # create empty container 818 | ges_start = bpy.context.selected_objects[0] 819 | ges_start.name = "Anchor Empty" 820 | ges_start.parent = ges_path 821 | ges_start.parent_type = 'OBJECT' 822 | 823 | 824 | # reset rotation on obj 825 | obj.rotation_euler[1] = math.radians(0) 826 | obj.rotation_euler[2] = math.radians(0) 827 | 828 | # add object to parent 829 | obj.parent = ges_path 830 | obj.parent_type = 'OBJECT' 831 | 832 | obj.data.bevel_depth = bpy.context.scene.GES_OT_Path.v_bevel 833 | 834 | ges_path.location = sn.matrix_world.to_translation() 835 | 836 | domData.unlink() 837 | 838 | def makemarkers(): 839 | mkrcnt = 0 # Counter for information 840 | 841 | # Load template objects 842 | mk = bpy.data.objects[bpy.context.scene.GES_OT_Path.v_mtemplate] 843 | 844 | # Create new collection 845 | if "GESMarkers" not in bpy.data.collections: 846 | collection = bpy.data.collections.new("GESMarkers") 847 | bpy.context.scene.collection.children.link(collection) 848 | 849 | # Cycle through GES trackpoints (except hidden ones) 850 | objects = bpy.data.objects["_GES_WORLD"].children 851 | 852 | for obj in objects: 853 | if obj.type == "MESH": 854 | if obj.data.name[0:5] == "Plane" and obj.visible_get(): 855 | # Trackpoint name and clean up 856 | newtext = obj.name 857 | spx = newtext.split(' ')[0].replace(".","") 858 | if spx.isdecimal(): 859 | newtext = newtext.replace(spx + ". ","") 860 | if mk.type == "EMPTY": 861 | # Create new empth 862 | mk2 = bpy.data.objects.new('Marker_' + newtext, None) 863 | else: 864 | # Create copy of marker 865 | mk2 = bpy.data.objects.new('Marker_' + newtext, mk.data.copy()) 866 | # Set location, scale and rotation for Parent object 867 | mk2.location = obj.matrix_world.translation 868 | mk2.scale = mk.scale 869 | mk2.rotation_euler = mk.rotation_euler 870 | 871 | # Add track to camera 872 | if str(bpy.context.scene.GES_OT_Path.v_mlookat) == "True": 873 | constraint = mk2.constraints.new(type='TRACK_TO') 874 | constraint.target = bpy.data.objects['Camera'] 875 | constraint.track_axis="TRACK_Z" 876 | # Move (link) to Collection 877 | bpy.data.collections["GESMarkers"].objects.link(mk2) 878 | # Clone children (really, we're doing that) 879 | kids = mk.children 880 | for kid in kids: 881 | 882 | k2 = bpy.data.objects.new( kid.name + '_' + newtext, kid.data.copy()) 883 | k2.matrix_local = kid.matrix_local 884 | if k2.type == 'FONT': # if a text object, set the text value to trackpoint name 885 | k2.data.body = newtext 886 | k2.parent = mk2 887 | 888 | bpy.data.collections["GESMarkers"].objects.link(k2) 889 | 890 | mkrcnt += 1 891 | ShowMessageBox( str(mkrcnt) + " Markers Created") 892 | 893 | def objecttokml(): 894 | 895 | wobj = bpy.data.objects['_GES_WORLD'] 896 | anc = wobj.children[0] #load first trackpoint 897 | ancp = wobj 898 | 899 | src_obj = bpy.context.active_object 900 | C = bpy.context 901 | 902 | # create a copy of object to modify for export 903 | obj = src_obj.copy() 904 | obj.data = src_obj.data.copy() 905 | obj.animation_data_clear() 906 | C.collection.objects.link(obj) 907 | 908 | src_obj.select_set(False) 909 | obj.select_set(True) #select the text obj 910 | bpy.context.view_layer.objects.active = obj 911 | bpy.context.view_layer.update() 912 | rmode = False 913 | 914 | # if object is a curve (a path) then covert the curve to a mesh 915 | if obj.parent: 916 | if obj.parent.name[0:9] == "_GES_PATH": 917 | obj.data.splines[0].resolution_u = 2 918 | 919 | override = bpy.context.copy() 920 | bpy.ops.object.convert(override,target='MESH') 921 | bpy.ops.object.parent_clear(type='CLEAR_KEEP_TRANSFORM') 922 | rmode = True 923 | 924 | bpy.data.objects[src_obj.name].select_set(False) 925 | bpy.data.objects[obj.name].select_set(True) 926 | 927 | bpy.ops.object.transform_apply(location=False, rotation=True, scale=True) 928 | 929 | # apply world transformation (#1) - using 1/100 scale and lat/long euler 930 | bpy.ops.transform.resize(value=(.10, .10, .10)) 931 | obj.rotation_euler[2] = anc.rotation_euler[2] 932 | bpy.ops.object.transform_apply(location=False, rotation=True, scale=True) 933 | 934 | earth = 6371010.1 #earth radius, in meters 935 | t_location = wobj.matrix_world.inverted() @ obj.location 936 | # get object starting location in world space 937 | tx = t_location.x 938 | ty = t_location.y 939 | tz = t_location.z 940 | 941 | pn = [] 942 | fn = [] 943 | 944 | bpy.context.view_layer.update() 945 | 946 | # create inverted matrix for world and anchor 947 | winvert = wobj.matrix_world.inverted() 948 | ainvert = anc.matrix_world.inverted() 949 | 950 | # cycle faces and extract vertices data with world and anchor matrix mutiplied 951 | for f in obj.data.polygons: 952 | pn.append("face") 953 | for v in f.vertices: 954 | if rmode == True: 955 | t_vertex = winvert @ ainvert @ obj.data.vertices[v].co 956 | else: 957 | t_vertex = winvert @ ainvert @ obj.data.vertices[v].co 958 | pn.append(str(tx + t_vertex.x ) + "," + str(ty + t_vertex.y ) + "," + str(tz + t_vertex.z )) 959 | 960 | firstface=True 961 | startagain ="0" 962 | 963 | # create kml header 964 | fn.append ("") 965 | fn.append ("") 966 | fn.append ("Exported from Blender") 967 | fn.append ('") 991 | fn.append ("" + str(obj.name) + "1") 992 | fn.append("#xstyle") 993 | fn.append ("") 994 | 995 | # write point parameters (lat/long/alt) for each face 996 | for f in pn: 997 | if f == "face": 998 | # for each face, start a new polygon 999 | if startagain != "0": 1000 | fn.append (startagain) 1001 | startagain = "0" 1002 | if firstface == False: 1003 | fn.append ("") 1004 | fn.append ("") 1005 | fn.append ("0absolute") 1006 | fn.append ("") 1007 | firstface = False 1008 | 1009 | else: 1010 | xoff = (float(f.split(',')[0]) * 100) + float(anc['X']) 1011 | yoff = (float(f.split(',')[1]) * 100) + float(anc['Y']) 1012 | zoff = (float(f.split(',')[2]) * 100) + float(anc['Z']) 1013 | 1014 | lat = float(anc['LAT']) 1015 | lon = float(anc['LNG']) 1016 | 1017 | edia2= earth + .00001 # used for non-spherical calculations - setting to small difference as GES uses globe 1018 | 1019 | # reverse blender coordinate infomation into lat/long/alt - fancy math (thanks google) 1020 | f = (earth - edia2) / earth 1021 | e_sq = f * (2 - f) 1022 | eps = e_sq / (1.0 - e_sq) 1023 | p = math.sqrt(xoff * xoff + yoff * yoff) 1024 | q = math.atan2((zoff * earth), (p * edia2)) 1025 | sin_q = math.sin(q) 1026 | cos_q = math.cos(q) 1027 | sin_q_3 = sin_q * sin_q * sin_q 1028 | cos_q_3 = cos_q * cos_q * cos_q 1029 | phi = math.atan2((zoff + eps * edia2 * sin_q_3), (p - e_sq * earth * cos_q_3)) 1030 | lam = math.atan2(yoff, xoff) 1031 | v = earth / math.sqrt(1.0 - e_sq * math.sin(phi) * math.sin(phi)) 1032 | h = (p / math.cos(phi)) - v 1033 | 1034 | ylat = math.degrees(phi) 1035 | ylon = math.degrees(lam) 1036 | 1037 | fn.append(str(ylon) + "," + str(ylat) + "," + str(h)) 1038 | if startagain == "0": 1039 | startagain = str(ylon) + "," + str(ylat) + "," + str(h) 1040 | 1041 | if startagain != "0": 1042 | fn.append (startagain) 1043 | # kml footer 1044 | fn.append ("") 1045 | fn.append ("") 1046 | fn.append ("") 1047 | 1048 | strout = "" 1049 | # spit out array into string 1050 | for f in fn: 1051 | strout += (str(f) + " ") 1052 | 1053 | # save the file 1054 | outputPath = bpy.path.abspath(bpy.context.scene.GES_OT_Path.p_objexpfolder + bpy.context.scene.GES_OT_Path.p_objexp + ".kml") 1055 | fileObject = open(outputPath, 'w') 1056 | fileObject.write(strout) 1057 | fileObject.close() 1058 | 1059 | # remove copied object 1060 | bpy.ops.object.delete() 1061 | 1062 | # set focus back to original object 1063 | bpy.data.objects[src_obj.name].select_set(True) 1064 | ShowMessageBox( str(bpy.context.scene.GES_OT_Path.p_objexp) + ".kml saved.") 1065 | 1066 | def rgb_to_hex(color): 1067 | 1068 | strip_n_pad = lambda stp: str(stp[2:]).zfill(2) 1069 | zcol = "".join([strip_n_pad(hex(int(col * 255))) for col in color]) 1070 | rcol = zcol[4:6] + zcol[2:4] + zcol[0:2] # earth format 1071 | 1072 | return rcol 1073 | 1074 | def prettyPrint(element, level=0): 1075 | ''' 1076 | Printing in elementTree requires a little massaging 1077 | Function taken from elementTree site: 1078 | http://effbot.org/zone/element-lib.htm#prettyprint 1079 | 1080 | ''' 1081 | indent = '\n' + level * ' ' 1082 | if len(element): 1083 | if not element.text or not element.text.strip(): 1084 | element.text = indent + ' ' 1085 | 1086 | if not element.tail or not element.tail.strip(): 1087 | element.tail = indent 1088 | 1089 | for element in element: 1090 | prettyPrint(element, level + 1) 1091 | 1092 | if not element.tail or not element.tail.strip(): 1093 | element.tail = indent 1094 | 1095 | else: 1096 | if level and (not element.tail or not element.tail.strip()): 1097 | element.tail = indent 1098 | 1099 | return element 1100 | 1101 | 1102 | def register(): 1103 | bpy.utils.register_class(GES_PT_ImportPanel) 1104 | bpy.utils.register_class(GES_PT_KMLPanel) 1105 | bpy.utils.register_class(GES_PT_ObjectKMLPanel) 1106 | bpy.utils.register_class(GES_PT_MarkerPanel) 1107 | bpy.utils.register_class(GES_PT_InfoPanel) 1108 | bpy.utils.register_class(GES_OT_Path) 1109 | bpy.utils.register_class(preobjKML) 1110 | bpy.utils.register_class(preKML) 1111 | bpy.utils.register_class(preGES) 1112 | bpy.utils.register_class(preMarker) 1113 | bpy.utils.register_class(isvoid) 1114 | 1115 | bpy.types.Scene.GES_OT_Path = bpy.props.PointerProperty(type=GES_OT_Path) 1116 | 1117 | def unregister(): 1118 | bpy.utils.unregister_class(GES_PT_ImportPanel) 1119 | bpy.utils.unregister_class(GES_PT_KMLPanel) 1120 | bpy.utils.unregister_class(GES_PT_ObjectKMLPanel) 1121 | bpy.utils.unregister_class(GES_PT_MarkerPanel) 1122 | bpy.utils.unregister_class(GES_PT_InfoPanel) 1123 | bpy.utils.unregister_class(GES_OT_Path) 1124 | bpy.utils.unregister_class(preobjKML) 1125 | bpy.utils.unregister_class(preKML) 1126 | bpy.utils.unregister_class(preGES) 1127 | bpy.utils.unregister_class(preMarker) 1128 | bpy.utils.unregister_class(isvoid) 1129 | 1130 | if __name__ == "__main__": 1131 | register() -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Modified MIT License 2 | 3 | Copyright (c) 2021 imagiscope 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use the Software, and to permit persons to whom the Software is 9 | furnished to do so, subject to the following conditions: 10 | 11 | 1. Not for resale (use of software tools for revenue generation excluded). 12 | 2. Credit to: "https://www.youtube.com/c/ImagiscopeTech" 13 | 3. Notification of commercial use to author. 14 | 15 | The above copyright notice and this permission notice shall be included in all 16 | copies or substantial portions of the Software. 17 | 18 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 19 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 20 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 21 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 22 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 23 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 24 | SOFTWARE. 25 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # EarthStudioTools 2 | Blender Panel to import Google Earth Studio, KML Routes, and 3D Masking 3 | 4 | Check out videos for usage: https://www.youtube.com/imagiscopetech 5 | 6 | Rob 7 | -------------------------------------------------------------------------------- /Version 1.2 update: -------------------------------------------------------------------------------- 1 | Version 1.2.2: 2 | - Added code for new KML layout 3 | 4 | Version 1.2.1: 5 | - fixed Marker Maker code 6 | 7 | Version 1.2: 8 | - New 3D object to KML 9 | - Updated Marker Maker 10 | - various fixes and error control (for international users, deleting object, etc) 11 | - removed export KML (as 3D object to KML achieves better results) 12 | 13 | Version 1.1: 14 | - NEW Trackpoint Marker Tool to generate Markers for Points of Interest based on a template you design (text will use name of trackpoint) 15 | - Update to KML importer - will now import KML files from Google My Maps 16 | - Improved messaging - alert on common error: when no trackpoints are in the JSON file, import will abort and message will be displayed to correct the issue (add trackpoint and export JSON file from GES). 17 | - Improvements for multiple KML files (fixes alignment issues) 18 | --------------------------------------------------------------------------------