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

Last change on this file since 1326 was 1307, checked in by sz, 5 months 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
RevLine 
[497]1bl_info = {
2    "name": "Framsticks POV import & camera manipulator",
3    "author": "Szymon Ulatowski, jm soler",
[1307]4    "version": (1, 0, 1),
5    "blender": (2, 80, 0),
[497]6    "location": "File > Import-Export",
[498]7    "category": "Import-Export",
[499]8    "description": "Imports POV-Ray files generated by Framsticks and exports Blender camera to POV-Ray files",
[498]9    "wiki_url": "http://www.framsticks.com/3d-animations-in-blender"
10}
[497]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]
[629]72    if val.find('.')!=-1 or val.find('e')!=-1:
[497]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:
[1307]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))
[497]159
[1307]160        if caps:
161            faces.append(tuple(range(n-1,-1,-1)))
162            faces.append(tuple(range(n,2*n)))
[497]163
[1307]164        self.mesh.from_pydata(verts,[],faces)
165        self.mesh.update()
[497]166
[1307]167    def makeObject(self, name):
168        o = bpy.data.objects.new(name, self.mesh)
169        bpy.context.collection.objects.link(o)
170        return o
[497]171
172# build or update blender objects from a POV file
173def updateBlender(SceneParent,num):
[1307]174    global Folder, Files, Current, RecentlyCreated, RecentlyDisappeared, CylinderMesh, LinkedMesh
[497]175    Incoming=analysePOV(os.path.join(Folder,Files[num]))
[578]176    RecentlyCreated=[]
177    RecentlyDisappeared=[]
[497]178    for oname,obj in Incoming.items():
179        if not oname in Current:
180            # add object properties
181            print('Creature added:',oname)
[578]182            RecentlyCreated.append(oname)
[497]183            newobj=[] # will contain: [ parent, joint0, joint1, ... ]
184            Current[oname]=newobj
185            # create new blender objects
186            bcrea=bpy.data.objects.new(oname,None)
[1307]187            bpy.context.collection.objects.link(bcrea)
[497]188            bcrea.parent=SceneParent
189            newobj.append(bcrea)
190            for j in obj['joints']:
[1307]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)
[497]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)
[578]224    for oname,obj in Current.items():
225        if not oname in Incoming:
226            RecentlyDisappeared.append(oname)
227            print('Creature disappeared:',oname)
[497]228
[1307]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           
[497]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
[578]240       global  RecentlyCreated, RecentlyDisappeared
[497]241       global  SceneParent
242       global  SkipFrames
[1307]243       global  CylinderMesh
[497]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
[1307]253       CylinderMesh = None
[497]254       SceneParent=bpy.data.objects.new("Framsticks_"+str(FirstFrame),None)
[1307]255       bpy.context.collection.objects.link(SceneParent)
[497]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]))
[578]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:
[1307]276                       setHideWithKeyframe(bo,True)
[578]277               for oname in RecentlyDisappeared:
278                   obj=Current[oname]
279                   for bo in obj:
[1307]280                       setHideWithKeyframe(bo,False)
[578]281               bpy.context.scene.frame_set(FirstFrame-FirstPOV+k)
282               for oname in RecentlyCreated:
283                   obj=Current[oname]
284                   for bo in obj:
[1307]285                       setHideWithKeyframe(bo,False)
[578]286               for oname in RecentlyDisappeared:
287                   obj=Current[oname]
288                   for bo in obj:
[1307]289                       setHideWithKeyframe(bo,True)
[578]290                   Current.pop(oname)
[497]291
[578]292
[497]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
[630]313def povUpdateScene(obj,writepath):
[497]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
[630]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
[497]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
[630]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)
[497]346
[630]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
[497]357####################################
358
359class FramsticksPOVImporter(bpy.types.Operator, ImportHelper):
[499]360    """Load a collection of Framsticks POV files"""
[497]361    bl_idname = "framspov.import"
362    bl_label = "Import Framsticks POV"
363    bl_options = {'UNDO'}
364
[1307]365    files: CollectionProperty(name="File Path",
[497]366                          description="File path used for importing",
367                          type=bpy.types.OperatorFileListElement)
[1307]368    directory: StringProperty()
[497]369
[1307]370    framspov_skip: bpy.props.IntProperty(name="Frame step",min=1,max=100)
[497]371
[1307]372    framspov_linkedmesh: bpy.props.BoolProperty(name="Linked cylinder mesh", description="Use a single shared cylinder mesh for all sticks", default=True)
373
[497]374    filename_ext = ".pov"
[1307]375    filter_glob: StringProperty(default="*.pov", options={'HIDDEN'})
[497]376
377    def execute(self, context):
[1307]378        global FirstFrame, FrameCount,FileName,SkipFrames,LinkedMesh
[497]379        FirstFrame = bpy.context.scene.frame_current
380        FrameCount = 9999
381        SkipFrames = self.framspov_skip
[1307]382        LinkedMesh = self.framspov_linkedmesh
[497]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')
[630]405        row = layout.row()
406        row.operator("framspov.writecamerapath",icon='SCRIPT')
[497]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):
[630]414        povUpdateScene(context.object,False)
[497]415        return{'FINISHED'}
416
[630]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
[497]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)
[630]431    bpy.utils.register_class(VIEW3D_OT_WritePOVCameraPath)
[497]432    bpy.utils.register_class(OBJECT_PT_framspov)
[1307]433    bpy.types.TOPBAR_MT_file_import.append(menu_func_import)
[497]434
435def unregister():
436    bpy.utils.unregister_class(FramsticksPOVImporter)
437    bpy.utils.unregister_class(VIEW3D_OT_UpdatePOVCamera)
[630]438    bpy.utils.unregister_class(VIEW3D_OT_WritePOVCameraPath)
[497]439    bpy.utils.unregister_class(OBJECT_PT_framspov)
[1307]440    bpy.types.TOPBAR_MT_file_import.remove(menu_func_import)
Note: See TracBrowser for help on using the repository browser.