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", |
---|
6 | "category": "Import-Export", |
---|
7 | "description": "Imports POV-Ray files generated by Framsticks and exports Blender camera to POV-Ray files", |
---|
8 | "wiki_url": "http://www.framsticks.com/3d-animations-in-blender" |
---|
9 | } |
---|
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 or val.find('e')!=-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: |
---|
142 | def __init__(self,nom='cylinderModel',type='cyl',r1=0.1, r2=0.1,h=1.0, n=8,smooth=1): |
---|
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): |
---|
175 | global Folder, Files, Current, RecentlyCreated, RecentlyDisappeared |
---|
176 | Incoming=analysePOV(os.path.join(Folder,Files[num])) |
---|
177 | RecentlyCreated=[] |
---|
178 | RecentlyDisappeared=[] |
---|
179 | for oname,obj in Incoming.items(): |
---|
180 | if not oname in Current: |
---|
181 | # add object properties |
---|
182 | print('Creature added:',oname) |
---|
183 | RecentlyCreated.append(oname) |
---|
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) |
---|
221 | for oname,obj in Current.items(): |
---|
222 | if not oname in Incoming: |
---|
223 | RecentlyDisappeared.append(oname) |
---|
224 | print('Creature disappeared:',oname) |
---|
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 |
---|
231 | global RecentlyCreated, RecentlyDisappeared |
---|
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])) |
---|
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) |
---|
284 | |
---|
285 | |
---|
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,writepath): |
---|
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 | |
---|
314 | if writepath: |
---|
315 | f=open(os.path.join(Folder,'camerapath.inc'),'w',encoding='latin-1') |
---|
316 | f.write("#local CameraPathFirst=1;\n") |
---|
317 | f.write("#local CameraPath=array["+str(len(Files))+"*2]\n") |
---|
318 | f.write("{\n") |
---|
319 | |
---|
320 | FirstFrame=obj.framspov_frame |
---|
321 | FrameCount=obj.framspov_count |
---|
322 | for k in sorted(Files.keys()): |
---|
323 | #bpy.context.scene.frame_current=FirstFrame-FirstPOV+k |
---|
324 | bpy.context.scene.frame_set(FirstFrame-FirstPOV+k) |
---|
325 | if bpy.context.scene.frame_current >= FirstFrame+FrameCount: |
---|
326 | break |
---|
327 | print("Frame %d - updating camera in %s" % (bpy.context.scene.frame_current,Files[k])) |
---|
328 | cam=framsCameraFromObj(bpy.context.scene.camera) |
---|
329 | cam[0]-=obj.location.x |
---|
330 | cam[1]-=obj.location.y |
---|
331 | cam[2]-=obj.location.z |
---|
332 | cam[3]-=obj.location.x |
---|
333 | cam[4]-=obj.location.y |
---|
334 | cam[5]-=obj.location.z |
---|
335 | if writepath: |
---|
336 | f.write((" <%g,%g,%g>,<%g,%g,%g>," % tuple(cam))+" //"+Files[k]+"\n") |
---|
337 | else: |
---|
338 | povUpdateFile(os.path.join(Folder,Files[k]),cam) |
---|
339 | |
---|
340 | if writepath: |
---|
341 | f.write(''' |
---|
342 | } |
---|
343 | #if ((AnimFrame>=CameraPathFirst) & ((AnimFrame-CameraPathFirst)<(dimension_size(CameraPath,1)/2))) |
---|
344 | #local i=2*(AnimFrame-CameraPathFirst); |
---|
345 | Camera(CameraPath[i],CameraPath[i+1]) |
---|
346 | #end |
---|
347 | ''') |
---|
348 | f.close() |
---|
349 | |
---|
350 | #################################### |
---|
351 | |
---|
352 | class FramsticksPOVImporter(bpy.types.Operator, ImportHelper): |
---|
353 | """Load a collection of Framsticks POV files""" |
---|
354 | bl_idname = "framspov.import" |
---|
355 | bl_label = "Import Framsticks POV" |
---|
356 | bl_options = {'UNDO'} |
---|
357 | |
---|
358 | files = CollectionProperty(name="File Path", |
---|
359 | description="File path used for importing", |
---|
360 | type=bpy.types.OperatorFileListElement) |
---|
361 | directory = StringProperty() |
---|
362 | |
---|
363 | framspov_skip = bpy.props.IntProperty(name="Frame step",min=1,max=100) |
---|
364 | |
---|
365 | filename_ext = ".pov" |
---|
366 | filter_glob = StringProperty(default="*.pov", options={'HIDDEN'}) |
---|
367 | |
---|
368 | def execute(self, context): |
---|
369 | global FirstFrame, FrameCount,FileName,SkipFrames |
---|
370 | FirstFrame = bpy.context.scene.frame_current |
---|
371 | FrameCount = 9999 |
---|
372 | SkipFrames = self.framspov_skip |
---|
373 | framsImport(os.path.join(self.directory, self.files[0].name)) |
---|
374 | return {'FINISHED'} |
---|
375 | |
---|
376 | |
---|
377 | def menu_func_import(self, context): |
---|
378 | self.layout.operator(FramsticksPOVImporter.bl_idname, text="Framsticks POV (.pov)") |
---|
379 | |
---|
380 | class OBJECT_PT_framspov(bpy.types.Panel): |
---|
381 | bl_label = "Framsticks POV" |
---|
382 | bl_space_type = "PROPERTIES" |
---|
383 | bl_region_type = "WINDOW" |
---|
384 | bl_context = "object" |
---|
385 | |
---|
386 | @classmethod |
---|
387 | def poll(cls, context): |
---|
388 | obj=context.object |
---|
389 | return obj.framspov_file!='' |
---|
390 | |
---|
391 | def draw(self, context): |
---|
392 | layout = self.layout |
---|
393 | row = layout.row() |
---|
394 | row.operator("framspov.updatecam",icon='SCRIPT') |
---|
395 | row = layout.row() |
---|
396 | row.operator("framspov.writecamerapath",icon='SCRIPT') |
---|
397 | |
---|
398 | |
---|
399 | class VIEW3D_OT_UpdatePOVCamera(bpy.types.Operator): |
---|
400 | bl_idname = "framspov.updatecam" |
---|
401 | bl_label = "Update POV camera" |
---|
402 | |
---|
403 | def execute(self, context): |
---|
404 | povUpdateScene(context.object,False) |
---|
405 | return{'FINISHED'} |
---|
406 | |
---|
407 | class VIEW3D_OT_WritePOVCameraPath(bpy.types.Operator): |
---|
408 | bl_idname = "framspov.writecamerapath" |
---|
409 | bl_label = "Write camerapath.inc" |
---|
410 | |
---|
411 | def execute(self, context): |
---|
412 | povUpdateScene(context.object,True) |
---|
413 | return{'FINISHED'} |
---|
414 | |
---|
415 | def register(): |
---|
416 | bpy.types.Object.framspov_file=bpy.props.StringProperty(name="Name of the first POV file") |
---|
417 | bpy.types.Object.framspov_frame=bpy.props.IntProperty(name="First frame",min=0,max=999999) |
---|
418 | bpy.types.Object.framspov_count=bpy.props.IntProperty(name="Number of frames",min=0,max=999999) |
---|
419 | bpy.utils.register_class(FramsticksPOVImporter) |
---|
420 | bpy.utils.register_class(VIEW3D_OT_UpdatePOVCamera) |
---|
421 | bpy.utils.register_class(VIEW3D_OT_WritePOVCameraPath) |
---|
422 | bpy.utils.register_class(OBJECT_PT_framspov) |
---|
423 | bpy.types.INFO_MT_file_import.append(menu_func_import) |
---|
424 | |
---|
425 | def unregister(): |
---|
426 | bpy.utils.unregister_class(FramsticksPOVImporter) |
---|
427 | bpy.utils.unregister_class(VIEW3D_OT_UpdatePOVCamera) |
---|
428 | bpy.utils.unregister_class(VIEW3D_OT_WritePOVCameraPath) |
---|
429 | bpy.utils.unregister_class(OBJECT_PT_framspov) |
---|
430 | bpy.types.INFO_MT_file_import.remove(menu_func_import) |
---|
431 | |
---|