[497] | 1 | bl_info = { |
---|
| 2 | "name": "Framsticks POV import & camera manipulator", |
---|
| 3 | "author": "Szymon Ulatowski, jm soler", |
---|
| 4 | "blender": (2, 70, 0), |
---|
| 5 | "location": "File > Import-Export", |
---|
[498] | 6 | "category": "Import-Export", |
---|
[499] | 7 | "description": "Imports POV-Ray files generated by Framsticks and exports Blender camera to POV-Ray files", |
---|
[498] | 8 | "wiki_url": "http://www.framsticks.com/3d-animations-in-blender" |
---|
| 9 | } |
---|
[497] | 10 | |
---|
| 11 | import os, math, bpy, re, copy, functools |
---|
| 12 | from bpy_extras.io_utils import ImportHelper |
---|
| 13 | from bpy.props import (CollectionProperty, StringProperty, BoolProperty, EnumProperty, FloatProperty) |
---|
| 14 | |
---|
| 15 | re_field=re.compile('^#declare field_([^_]+)_([^=]+)=(.*)$') |
---|
| 16 | re_object=re.compile('^BeginObject\(\)$') |
---|
| 17 | re_part=re.compile('^BeginObject\(\)$') |
---|
| 18 | re_partgeo=re.compile('^PartGeometry\(<([^>]+)>,<([^>]+)>\)$') |
---|
| 19 | re_joint=re.compile('^BeginJoint\(([0-9]+),([0-9]+)\)$') |
---|
| 20 | re_jointgeo=re.compile('^JointGeometry\(<([^>]+)>,<([^>]+)>,<([^>]+)>\)$') |
---|
| 21 | re_neuro=re.compile('^BeginNeuro\(([^)])\)$') |
---|
| 22 | |
---|
| 23 | Folder = "" |
---|
| 24 | FirstPOV=-1 |
---|
| 25 | LastPOV=-1 |
---|
| 26 | CREATURES={} |
---|
| 27 | |
---|
| 28 | # assumes the filename is "*_number.pov" |
---|
| 29 | def numFromFilename(f): |
---|
| 30 | try: |
---|
| 31 | return int(f[f.find('_')+1:f.find('.')]) |
---|
| 32 | except: |
---|
| 33 | return -1 |
---|
| 34 | |
---|
| 35 | # creates a global dictionary of filenames ("*_number.pov"), where key=number, updates FirstPOV/LastPOV |
---|
| 36 | def scanDir(startfile): |
---|
| 37 | global Files, Folder, FileName |
---|
| 38 | global FirstPOV, LastPOV |
---|
| 39 | |
---|
| 40 | Files={} |
---|
| 41 | Folder, FileName=os.path.split(startfile) |
---|
| 42 | |
---|
| 43 | #print("startfile=",startfile,"Folder=",Folder,"FileName=",FileName) |
---|
| 44 | |
---|
| 45 | underscore=FileName.find("_") |
---|
| 46 | if underscore==-1: |
---|
| 47 | return |
---|
| 48 | if FileName.find(".pov")==-1: |
---|
| 49 | return |
---|
| 50 | |
---|
| 51 | FirstPOV=numFromFilename(FileName) |
---|
| 52 | LastPOV=FirstPOV |
---|
| 53 | |
---|
| 54 | for f in os.listdir(Folder): |
---|
| 55 | if not f.endswith('.pov'): |
---|
| 56 | continue |
---|
| 57 | if not f.startswith(FileName[:underscore+1]): |
---|
| 58 | continue |
---|
| 59 | num=numFromFilename(f) |
---|
| 60 | if num<0: |
---|
| 61 | continue |
---|
| 62 | Files[num]=f |
---|
| 63 | if num>LastPOV: |
---|
| 64 | LastPOV=num |
---|
| 65 | #print("N=",len(Files)) |
---|
| 66 | |
---|
| 67 | def extractValue(val): |
---|
| 68 | if val[0]=='"': |
---|
| 69 | # "string" |
---|
| 70 | return val[1:-1] |
---|
| 71 | if val.find('.')!=-1: |
---|
| 72 | # floating point |
---|
| 73 | return float(val) |
---|
| 74 | else: |
---|
| 75 | # integer |
---|
| 76 | return int(val) |
---|
| 77 | |
---|
| 78 | def floatList(str): |
---|
| 79 | return [float(x) for x in str.split(',')] |
---|
| 80 | |
---|
| 81 | def analysePOV(fname): |
---|
| 82 | global re_field,re_object,re_part,re_partgeo,re_joint,re_jointgeo,re_neuro |
---|
| 83 | f=open(fname,'r',encoding='latin-1') |
---|
| 84 | POVlines=f.readlines() |
---|
| 85 | f.close() |
---|
| 86 | tmpfields={} |
---|
| 87 | objects={} |
---|
| 88 | for line in POVlines: |
---|
| 89 | m=re_field.match(line) |
---|
| 90 | if m: |
---|
| 91 | value=m.group(3) |
---|
| 92 | if value.endswith(';'): |
---|
| 93 | value=value[:-1] |
---|
| 94 | value=extractValue(value) |
---|
| 95 | objname=m.group(1) # creature,m,p,j,n |
---|
| 96 | fieldname=m.group(2) |
---|
| 97 | if not objname in tmpfields: |
---|
| 98 | tmpfields[objname]={} |
---|
| 99 | tmpfields[objname][fieldname]=value |
---|
| 100 | #print("obj=",m.group(1)," name=",m.group(2)," value=",value) |
---|
| 101 | m=re_object.match(line) |
---|
| 102 | if m: |
---|
| 103 | objkey=tmpfields['Creature']['name']+'_'+tmpfields['Creature']['uid'] |
---|
| 104 | objkey=objkey.replace(' ','_') |
---|
| 105 | recentobj={'fields':copy.deepcopy(tmpfields['Creature']),'parts':[],'joints':[],'neurons':[]} |
---|
| 106 | objects[objkey]=recentobj |
---|
| 107 | m=re_jointgeo.match(line) |
---|
| 108 | if m: |
---|
| 109 | joint={'StickLoc1':floatList(m.group(1)), |
---|
| 110 | 'StickLoc2':floatList(m.group(2)), |
---|
| 111 | 'StickRot':floatList(m.group(3)), |
---|
| 112 | 'fields':copy.deepcopy(tmpfields['j'])} |
---|
| 113 | #print(joint) |
---|
| 114 | recentobj['joints'].append(joint) |
---|
| 115 | m=re_partgeo.match(line) |
---|
| 116 | if m: |
---|
| 117 | part={'Loc':floatList(m.group(1)), |
---|
| 118 | 'Rot':floatList(m.group(2)), |
---|
| 119 | 'fields':copy.deepcopy(tmpfields['p'])} |
---|
| 120 | #print(joint) |
---|
| 121 | recentobj['parts'].append(part) |
---|
| 122 | #print(tmpfields) |
---|
| 123 | #print(objects) |
---|
| 124 | #print(json.dumps(objects,indent=4)) |
---|
| 125 | return objects |
---|
| 126 | |
---|
| 127 | # vector length |
---|
| 128 | def vecLength(p1,p2): |
---|
| 129 | p2[0]=p2[0]-p1[0] |
---|
| 130 | p2[1]=p2[1]-p1[1] |
---|
| 131 | p2[2]=p2[2]-p1[2] |
---|
| 132 | return (p2[0]**2+p2[1]**2+p2[2]**2)**0.5 |
---|
| 133 | |
---|
| 134 | def vecSub(p1,p2): |
---|
| 135 | return [p1[0]-p2[0], p1[1]-p2[1], p1[2]-p2[2]] |
---|
| 136 | |
---|
| 137 | def vecMul(p1,m): |
---|
| 138 | return [p1[0]*m, p1[1]*m, p1[2]*m] |
---|
| 139 | |
---|
| 140 | # create an object containing reference to blender cylinder object |
---|
| 141 | class cylindre: |
---|
[499] | 142 | def __init__(self,nom='cylinderModel',type='cyl',r1=0.1, r2=0.1,h=1.0, n=8,smooth=1): |
---|
[497] | 143 | me=bpy.data.meshes.new(name=nom) |
---|
| 144 | r=[r1,r2] |
---|
| 145 | verts=[] |
---|
| 146 | for i in range(0,2): |
---|
| 147 | for j in range(0,n): |
---|
| 148 | z=math.sin(j*math.pi*2/(n))*r[i] |
---|
| 149 | y=math.cos(j*math.pi*2/(n))*r[i] |
---|
| 150 | x=float(i)*h |
---|
| 151 | verts.append((x,y,z)) |
---|
| 152 | |
---|
| 153 | vlist=[v for v in range(n)] |
---|
| 154 | vlist.append(0) |
---|
| 155 | faces=[] |
---|
| 156 | for i in range(n): |
---|
| 157 | faces.append((vlist[i],vlist[i+1],vlist[i+1]+n,vlist[i]+n)) |
---|
| 158 | |
---|
| 159 | if type=='cyl': |
---|
| 160 | pos=[[0.0,0.0,0.0],[0.0,0.0,h]] |
---|
| 161 | verts.append((pos[0][0],pos[0][1],pos[0][2])) |
---|
| 162 | verts.append((pos[1][0],pos[1][1],pos[1][2])) |
---|
| 163 | |
---|
| 164 | for i in range(n): |
---|
| 165 | faces.append((vlist[i],vlist[i+1],len(vlist)-2)) |
---|
| 166 | faces.append((vlist[i],vlist[i+1],len(vlist)-1)) |
---|
| 167 | |
---|
| 168 | me.from_pydata(verts,[],faces) |
---|
| 169 | me.update() |
---|
| 170 | self.objet=bpy.data.objects.new(nom,me) |
---|
| 171 | bpy.context.scene.objects.link(self.objet) |
---|
| 172 | |
---|
| 173 | # build or update blender objects from a POV file |
---|
| 174 | def updateBlender(SceneParent,num): |
---|
[578] | 175 | global Folder, Files, Current, RecentlyCreated, RecentlyDisappeared |
---|
[497] | 176 | Incoming=analysePOV(os.path.join(Folder,Files[num])) |
---|
[578] | 177 | RecentlyCreated=[] |
---|
| 178 | RecentlyDisappeared=[] |
---|
[497] | 179 | for oname,obj in Incoming.items(): |
---|
| 180 | if not oname in Current: |
---|
| 181 | # add object properties |
---|
| 182 | print('Creature added:',oname) |
---|
[578] | 183 | RecentlyCreated.append(oname) |
---|
[497] | 184 | newobj=[] # will contain: [ parent, joint0, joint1, ... ] |
---|
| 185 | Current[oname]=newobj |
---|
| 186 | # create new blender objects |
---|
| 187 | bcrea=bpy.data.objects.new(oname,None) |
---|
| 188 | bpy.context.scene.objects.link(bcrea) |
---|
| 189 | bcrea.parent=SceneParent |
---|
| 190 | newobj.append(bcrea) |
---|
| 191 | for j in obj['joints']: |
---|
| 192 | cyl=cylindre(oname+'_j','tube',0.1,0.1,1.0,6) |
---|
| 193 | cyl.objet.parent=bcrea |
---|
| 194 | newobj.append(cyl.objet) |
---|
| 195 | |
---|
| 196 | # update blender loc/rot/scale |
---|
| 197 | existing_b=Current[oname] |
---|
| 198 | if len(obj['joints']): |
---|
| 199 | avg_loc=vecMul(functools.reduce(lambda a,b: [a[0]+b[0],a[1]+b[1],a[2]+b[2]], [j['StickLoc1'] for j in obj['joints']]),1/len(obj['joints'])) |
---|
| 200 | elif len(obj['parts']): |
---|
| 201 | avg_loc=vecMul(functools.reduce(lambda a,b: [a[0]+b[0],a[1]+b[1],a[2]+b[2]], [p['Loc'] for p in obj['parts']]),1/len(obj['parts'])) |
---|
| 202 | else: |
---|
| 203 | avg_loc=[0,0,0] |
---|
| 204 | if len(existing_b)>0: |
---|
| 205 | existing_b[0].location=avg_loc |
---|
| 206 | existing_b[0].keyframe_insert(data_path='location',frame=bpy.context.scene.frame_current) |
---|
| 207 | for i in range(len(obj['joints'])): |
---|
| 208 | if i>=(len(existing_b)-1): |
---|
| 209 | continue # number of joints has changed -> ignore |
---|
| 210 | incoming_geo=obj['joints'][i] |
---|
| 211 | #print('incoming:',incoming_geo) |
---|
| 212 | bo=existing_b[i+1] # blender object |
---|
| 213 | scale=[vecLength(incoming_geo['StickLoc1'],incoming_geo['StickLoc2']), 1.0, 1.0] |
---|
| 214 | for xyz in [0,1,2]: |
---|
| 215 | getattr(bo,'location')[xyz]=vecSub(incoming_geo['StickLoc1'],avg_loc)[xyz] |
---|
| 216 | #getattr(bo,'location')[xyz]=incoming_geo['StickLoc1'][xyz] |
---|
| 217 | getattr(bo,'rotation_euler')[xyz]=incoming_geo['StickRot'][xyz] |
---|
| 218 | getattr(bo,'scale')[xyz]=scale[xyz] |
---|
| 219 | for field in ['location','rotation_euler','scale']: |
---|
| 220 | bo.keyframe_insert(data_path=field,frame=bpy.context.scene.frame_current) |
---|
[578] | 221 | for oname,obj in Current.items(): |
---|
| 222 | if not oname in Incoming: |
---|
| 223 | RecentlyDisappeared.append(oname) |
---|
| 224 | print('Creature disappeared:',oname) |
---|
[497] | 225 | |
---|
| 226 | # import a sequence of POV files, create object hiererchy, animate |
---|
| 227 | def framsImport(startfile): |
---|
| 228 | global FirstPOV, LastPOV, Files |
---|
| 229 | global Folder, FileName |
---|
| 230 | global Current, FirstFrame, FrameCount |
---|
[578] | 231 | global RecentlyCreated, RecentlyDisappeared |
---|
[497] | 232 | global SceneParent |
---|
| 233 | global SkipFrames |
---|
| 234 | |
---|
| 235 | scanDir(startfile) |
---|
| 236 | |
---|
| 237 | if len(Files)<1: |
---|
| 238 | print("No files found") |
---|
| 239 | return |
---|
| 240 | |
---|
| 241 | bpy.context.scene.frame_end=max(bpy.context.scene.frame_end,FirstFrame+FrameCount-1) |
---|
| 242 | |
---|
| 243 | SceneParent=bpy.data.objects.new("Framsticks_"+str(FirstFrame),None) |
---|
| 244 | bpy.context.scene.objects.link(SceneParent) |
---|
| 245 | SceneParent.framspov_file=startfile |
---|
| 246 | SceneParent.framspov_frame=FirstFrame |
---|
| 247 | SceneParent.framspov_count=FrameCount |
---|
| 248 | |
---|
| 249 | Current={} |
---|
| 250 | NextSkip=0 |
---|
| 251 | for k in sorted(Files.keys()): |
---|
| 252 | if k<NextSkip: |
---|
| 253 | continue |
---|
| 254 | NextSkip=k+SkipFrames |
---|
| 255 | bpy.context.scene.frame_set(FirstFrame-FirstPOV+k) |
---|
| 256 | if bpy.context.scene.frame_current >= FirstFrame+FrameCount: |
---|
| 257 | break |
---|
| 258 | print("Frame %d - loading POV %s" % (bpy.context.scene.frame_current,Files[k])) |
---|
[578] | 259 | updateBlender(SceneParent,k) |
---|
| 260 | if len(RecentlyDisappeared)>0 or len(RecentlyCreated)>0: |
---|
| 261 | bpy.context.scene.frame_set(FirstFrame-FirstPOV+k-1) |
---|
| 262 | for oname in RecentlyCreated: |
---|
| 263 | obj=Current[oname] |
---|
| 264 | for bo in obj: |
---|
| 265 | bo.hide=True |
---|
| 266 | bo.keyframe_insert(data_path="hide",frame=bpy.context.scene.frame_current) |
---|
| 267 | for oname in RecentlyDisappeared: |
---|
| 268 | obj=Current[oname] |
---|
| 269 | for bo in obj: |
---|
| 270 | bo.hide=False |
---|
| 271 | bo.keyframe_insert(data_path="hide",frame=bpy.context.scene.frame_current) |
---|
| 272 | bpy.context.scene.frame_set(FirstFrame-FirstPOV+k) |
---|
| 273 | for oname in RecentlyCreated: |
---|
| 274 | obj=Current[oname] |
---|
| 275 | for bo in obj: |
---|
| 276 | bo.hide=False |
---|
| 277 | bo.keyframe_insert(data_path="hide",frame=bpy.context.scene.frame_current) |
---|
| 278 | for oname in RecentlyDisappeared: |
---|
| 279 | obj=Current[oname] |
---|
| 280 | for bo in obj: |
---|
| 281 | bo.hide=True |
---|
| 282 | bo.keyframe_insert(data_path="hide",frame=bpy.context.scene.frame_current) |
---|
| 283 | Current.pop(oname) |
---|
[497] | 284 | |
---|
[578] | 285 | |
---|
[497] | 286 | ############################### |
---|
| 287 | |
---|
| 288 | def povUpdateFile(filename,cam): |
---|
| 289 | f=open(filename,'r',encoding='latin-1') |
---|
| 290 | lines=f.readlines() |
---|
| 291 | f.close() |
---|
| 292 | for i in range(len(lines)): |
---|
| 293 | line=lines[i] |
---|
| 294 | if line.startswith('Camera('): |
---|
| 295 | line='Camera(<%g,%g,%g>,<%g,%g,%g>)\n' % tuple(cam); |
---|
| 296 | lines[i]=line |
---|
| 297 | f=open(filename,'w',encoding='latin-1') |
---|
| 298 | f.writelines(lines) |
---|
| 299 | f.close() |
---|
| 300 | |
---|
| 301 | def framsCameraFromObj(obj): |
---|
| 302 | #print(obj.location.x) |
---|
| 303 | m=obj.matrix_local |
---|
| 304 | return [obj.location.x, obj.location.y, obj.location.z, obj.location.x-m[0][2], obj.location.y-m[1][2], obj.location.z-m[2][2]] |
---|
| 305 | |
---|
| 306 | def povUpdateScene(obj): |
---|
| 307 | global Folder,FirstFrame,FrameCount,Files,FirstPOV |
---|
| 308 | print("Updating scene %s" % obj.name) |
---|
| 309 | scanDir(obj.framspov_file) |
---|
| 310 | if len(Files)<1: |
---|
| 311 | print("No files found for "+obj.name) |
---|
| 312 | return |
---|
| 313 | FirstFrame=obj.framspov_frame |
---|
| 314 | FrameCount=obj.framspov_count |
---|
| 315 | for k in sorted(Files.keys()): |
---|
| 316 | #bpy.context.scene.frame_current=FirstFrame-FirstPOV+k |
---|
| 317 | bpy.context.scene.frame_set(FirstFrame-FirstPOV+k) |
---|
| 318 | if bpy.context.scene.frame_current >= FirstFrame+FrameCount: |
---|
| 319 | break |
---|
| 320 | print("Frame %d - updating camera in %s" % (bpy.context.scene.frame_current,Files[k])) |
---|
| 321 | cam=framsCameraFromObj(bpy.context.scene.camera) |
---|
| 322 | cam[0]-=obj.location.x |
---|
| 323 | cam[1]-=obj.location.y |
---|
| 324 | cam[2]-=obj.location.z |
---|
| 325 | cam[3]-=obj.location.x |
---|
| 326 | cam[4]-=obj.location.y |
---|
| 327 | cam[5]-=obj.location.z |
---|
| 328 | povUpdateFile(os.path.join(Folder,Files[k]),cam) |
---|
| 329 | |
---|
| 330 | #################################### |
---|
| 331 | |
---|
| 332 | class FramsticksPOVImporter(bpy.types.Operator, ImportHelper): |
---|
[499] | 333 | """Load a collection of Framsticks POV files""" |
---|
[497] | 334 | bl_idname = "framspov.import" |
---|
| 335 | bl_label = "Import Framsticks POV" |
---|
| 336 | bl_options = {'UNDO'} |
---|
| 337 | |
---|
| 338 | files = CollectionProperty(name="File Path", |
---|
| 339 | description="File path used for importing", |
---|
| 340 | type=bpy.types.OperatorFileListElement) |
---|
| 341 | directory = StringProperty() |
---|
| 342 | |
---|
| 343 | framspov_skip = bpy.props.IntProperty(name="Frame step",min=1,max=100) |
---|
| 344 | |
---|
| 345 | filename_ext = ".pov" |
---|
| 346 | filter_glob = StringProperty(default="*.pov", options={'HIDDEN'}) |
---|
| 347 | |
---|
| 348 | def execute(self, context): |
---|
| 349 | global FirstFrame, FrameCount,FileName,SkipFrames |
---|
| 350 | FirstFrame = bpy.context.scene.frame_current |
---|
| 351 | FrameCount = 9999 |
---|
| 352 | SkipFrames = self.framspov_skip |
---|
| 353 | framsImport(os.path.join(self.directory, self.files[0].name)) |
---|
| 354 | return {'FINISHED'} |
---|
| 355 | |
---|
| 356 | |
---|
| 357 | def menu_func_import(self, context): |
---|
| 358 | self.layout.operator(FramsticksPOVImporter.bl_idname, text="Framsticks POV (.pov)") |
---|
| 359 | |
---|
| 360 | class OBJECT_PT_framspov(bpy.types.Panel): |
---|
| 361 | bl_label = "Framsticks POV" |
---|
| 362 | bl_space_type = "PROPERTIES" |
---|
| 363 | bl_region_type = "WINDOW" |
---|
| 364 | bl_context = "object" |
---|
| 365 | |
---|
| 366 | @classmethod |
---|
| 367 | def poll(cls, context): |
---|
| 368 | obj=context.object |
---|
| 369 | return obj.framspov_file!='' |
---|
| 370 | |
---|
| 371 | def draw(self, context): |
---|
| 372 | layout = self.layout |
---|
| 373 | row = layout.row() |
---|
| 374 | row.operator("framspov.updatecam",icon='SCRIPT') |
---|
| 375 | |
---|
| 376 | |
---|
| 377 | class VIEW3D_OT_UpdatePOVCamera(bpy.types.Operator): |
---|
| 378 | bl_idname = "framspov.updatecam" |
---|
| 379 | bl_label = "Update POV camera" |
---|
| 380 | |
---|
| 381 | def execute(self, context): |
---|
| 382 | povUpdateScene(context.object) |
---|
| 383 | return{'FINISHED'} |
---|
| 384 | |
---|
| 385 | def register(): |
---|
| 386 | bpy.types.Object.framspov_file=bpy.props.StringProperty(name="Name of the first POV file") |
---|
| 387 | bpy.types.Object.framspov_frame=bpy.props.IntProperty(name="First frame",min=0,max=999999) |
---|
| 388 | bpy.types.Object.framspov_count=bpy.props.IntProperty(name="Number of frames",min=0,max=999999) |
---|
| 389 | bpy.utils.register_class(FramsticksPOVImporter) |
---|
| 390 | bpy.utils.register_class(VIEW3D_OT_UpdatePOVCamera) |
---|
| 391 | bpy.utils.register_class(OBJECT_PT_framspov) |
---|
| 392 | bpy.types.INFO_MT_file_import.append(menu_func_import) |
---|
| 393 | |
---|
| 394 | def unregister(): |
---|
| 395 | bpy.utils.unregister_class(FramsticksPOVImporter) |
---|
| 396 | bpy.utils.unregister_class(VIEW3D_OT_UpdatePOVCamera) |
---|
| 397 | bpy.utils.unregister_class(OBJECT_PT_framspov) |
---|
| 398 | bpy.types.INFO_MT_file_import.remove(menu_func_import) |
---|
| 399 | |
---|