source: blender-povray-animation/frams-pov-import.py

Last change on this file was 1307, checked in by sz, 4 weeks ago
  • compatibility update for modern blender versions (2.80 or later)
  • optional, enabled by default: use a single shared mesh instance for all sticks
  • the stick cylinder is now capped
File size: 15.7 KB
Line 
1bl_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
12import os, math, bpy, re, copy, functools
13from bpy_extras.io_utils import ImportHelper
14from bpy.props import (CollectionProperty, StringProperty, BoolProperty, EnumProperty, FloatProperty)
15
16re_field=re.compile('^#declare field_([^_]+)_([^=]+)=(.*)$')
17re_object=re.compile('^BeginObject\(\)$')
18re_part=re.compile('^BeginObject\(\)$')
19re_partgeo=re.compile('^PartGeometry\(<([^>]+)>,<([^>]+)>\)$')
20re_joint=re.compile('^BeginJoint\(([0-9]+),([0-9]+)\)$')
21re_jointgeo=re.compile('^JointGeometry\(<([^>]+)>,<([^>]+)>,<([^>]+)>\)$')
22re_neuro=re.compile('^BeginNeuro\(([^)])\)$')
23
24Folder = ""
25FirstPOV=-1
26LastPOV=-1
27CREATURES={}
28
29# assumes the filename is "*_number.pov"
30def 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
37def 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
68def 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
79def floatList(str):
80    return [float(x) for x in str.split(',')]
81
82def 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
129def 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
135def vecSub(p1,p2):
136    return [p1[0]-p2[0], p1[1]-p2[1], p1[2]-p2[2]]
137
138def 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
142class 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
173def 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
229def 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
236def 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
295def 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
308def 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
313def 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
359class 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
387def menu_func_import(self, context):
388    self.layout.operator(FramsticksPOVImporter.bl_idname, text="Framsticks POV (.pov)")
389
390class 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
409class 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
417class 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
425def 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
435def 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)
Note: See TracBrowser for help on using the repository browser.