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