3 """ Registration info for Blender menus:
7 Tip: 'Import an AC3D (.ac) file.'
10 __author__
= "Willian P. Germano"
11 __url__
= ("blender", "elysiun", "AC3D's homepage, http://www.ac3d.org",
12 "PLib 3d gaming lib, http://plib.sf.net")
13 __version__
= "2.43.1 2007-02-21"
16 This script imports AC3D models into Blender.
18 AC3D is a simple and affordable commercial 3d modeller also built with OpenGL.
19 The .ac file format is an easy to parse text format well supported,
20 for example, by the PLib 3d gaming library.
23 UV-textured meshes with hierarchy (grouping) information.
26 The url tag is irrelevant for Blender.
29 - Some objects may be imported with wrong normals due to wrong information in the model itself. This can be noticed by strange shading, like darker than expected parts in the model. To fix this, select the mesh with wrong normals, enter edit mode and tell Blender to recalculate the normals, either to make them point outside (the usual case) or inside.<br>
32 - display transp (toggle): if "on", objects that have materials with alpha < 1.0 are shown with translucency (transparency) in the 3D View.<br>
33 - subdiv (toggle): if "on", ac3d objects meant to be subdivided receive a SUBSURF modifier in Blender.<br>
34 - textures dir (string): if non blank, when imported texture paths are
35 wrong in the .ac file, Blender will also look for them at this dir.
38 - When looking for assigned textures, Blender tries in order: the actual
39 paths from the .ac file, the .ac file's dir and the default textures dir path
40 users can configure (see config options above).
43 # $Id: ac3d_import.py 10128 2007-02-22 20:19:58Z ianwill $
45 # --------------------------------------------------------------------------
46 # AC3DImport version 2.43.1 Feb 21, 2007
47 # Program versions: Blender 2.43 and AC3Db files (means version 0xb)
48 # changed: better triangulation of ngons, more fixes to support bad .ac files,
49 # option to display transp mats in 3d view, support "subdiv" tag (via SUBSURF modifier)
50 # --------------------------------------------------------------------------
51 # Thanks: Melchior Franz for extensive bug testing and reporting, making this
52 # version cope much better with old or bad .ac files, among other improvements;
53 # Stewart Andreason for reporting a serious crash.
54 # --------------------------------------------------------------------------
55 # ***** BEGIN GPL LICENSE BLOCK *****
57 # Copyright (C) 2004-2007: Willian P. Germano, wgermano _at_ ig.com.br
59 # This program is free software; you can redistribute it and/or
60 # modify it under the terms of the GNU General Public License
61 # as published by the Free Software Foundation; either version 2
62 # of the License, or (at your option) any later version.
64 # This program is distributed in the hope that it will be useful,
65 # but WITHOUT ANY WARRANTY; without even the implied warranty of
66 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
67 # GNU General Public License for more details.
69 # You should have received a copy of the GNU General Public License
70 # along with this program; if not, write to the Free Software Foundation,
71 # Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
73 # ***** END GPL LICENCE BLOCK *****
74 # --------------------------------------------------------------------------
76 from math
import radians
79 from Blender
import Scene
, Object
, Mesh
, Lamp
, Registry
, sys
as bsys
, Window
, Image
, Material
, Modifier
80 from Blender
.sys
import dirsep
81 from Blender
.Mathutils
import Vector
, Matrix
, Euler
82 from Blender
.Geometry
import PolyFill
84 # Default folder for AC3D textures, to override wrong paths, change to your
85 # liking or leave as "":
93 'DISPLAY_TRANSP': 'Turn transparency on in the 3d View for objects using materials with alpha < 1.0.',
94 'SUBDIV': 'Apply a SUBSURF modifier to objects meant to appear subdivided.',
95 'TEXTURES_DIR': 'Additional folder to look for missing textures.'
98 def update_registry():
99 global TEXTURES_DIR
, DISPLAY_TRANSP
100 rd
= dict([('tooltips', tooltips
), ('TEXTURES_DIR', TEXTURES_DIR
), ('DISPLAY_TRANSP', DISPLAY_TRANSP
), ('SUBDIV', SUBDIV
)])
101 Registry
.SetKey('ac3d_import', rd
, True)
103 rd
= Registry
.GetKey('ac3d_import', True)
109 TEXTURES_DIR
= rd
['TEXTURES_DIR']
110 DISPLAY_TRANSP
= rd
['DISPLAY_TRANSP']
111 SUBDIV
= rd
['SUBDIV']
114 else: update_registry()
117 oldtexdir
= TEXTURES_DIR
118 if dirsep
== '/': TEXTURES_DIR
= TEXTURES_DIR
.replace('\\', '/')
119 if TEXTURES_DIR
[-1] != dirsep
: TEXTURES_DIR
= "%s%s" % (TEXTURES_DIR
, dirsep
)
120 if oldtexdir
!= TEXTURES_DIR
: update_registry()
124 rd
= Registry
.GetKey('General', True)
126 if rd
.has_key('verbose'):
127 VERBOSE
= rd
['verbose']
132 # Matrix to align ac3d's coordinate system with Blender's one,
133 # it's a -90 degrees rotation around the x axis:
134 AC_TO_BLEND_MATRIX
= Matrix([1, 0, 0], [0, 0, 1], [0, -1, 0])
147 AC_OB_BAD_TYPES_LIST
= [] # to hold references to unknown (wrong) ob types
151 if VERBOSE
: print msg
153 def euler_in_radians(eul
):
154 "Used while there's a bug in the BPY API"
155 eul
.x
= radians(eul
.x
)
156 eul
.y
= radians(eul
.y
)
157 eul
.z
= radians(eul
.z
)
162 def __init__(self
, type):
183 self
.bl_obj
= None # the actual Blender object created from this data
187 def __init__(self
, filename
):
191 self
.scene
= Scene
.GetCurrent()
195 self
.importdir
= bsys
.dirname(filename
)
197 file = open(filename
, 'r')
198 except IOError, (errno
, strerror
):
199 errmsg
= "IOError #%s: %s" % (errno
, strerror
)
200 Blender
.Draw
.PupMenu('ERROR: %s' % errmsg
)
203 header
= file.read(5)
204 header
, version
= header
[:4], header
[-1]
207 errmsg
= 'AC3D header not found (invalid file)'
208 Blender
.Draw
.PupMenu('ERROR: %s' % errmsg
)
212 inform('AC3D file version 0x%s.' % version
)
213 inform('This importer is for version 0xb, so it may fail.')
215 self
.token
= {'OBJECT': self
.parse_obj
,
216 'numvert': self
.parse_vert
,
217 'numsurf': self
.parse_surf
,
218 'name': self
.parse_name
,
219 'data': self
.parse_data
,
220 'kids': self
.parse_kids
,
221 'loc': self
.parse_loc
,
222 'rot': self
.parse_rot
,
223 'MATERIAL': self
.parse_mat
,
224 'texture': self
.parse_tex
,
225 'texrep': self
.parse_texrep
,
226 'texoff': self
.parse_texoff
,
227 'subdiv': self
.parse_subdiv
,
228 'crease': self
.parse_crease
}
232 self
.kidsnumlist
= []
235 self
.lines
= file.readlines()
236 self
.lines
.append('')
240 self
.testAC3DImport()
242 def parse_obj(self
, value
):
243 kidsnumlist
= self
.kidsnumlist
245 while not kidsnumlist
[-1]:
248 self
.dad
= self
.dad
.dad
250 inform('Ignoring unexpected data at end of file.')
251 return -1 # bad file with more objects than reported
253 if value
in AC_OB_TYPES
:
254 new
= Obj(AC_OB_TYPES
[value
])
256 if value
not in AC_OB_BAD_TYPES_LIST
:
257 AC_OB_BAD_TYPES_LIST
.append(value
)
258 inform('Unexpected object type keyword: "%s". Assuming it is of type: "poly".' % value
)
259 new
= Obj(AC_OB_TYPES
['poly'])
262 self
.objlist
.append(new
)
264 def parse_kids(self
, value
):
267 self
.kidsnumlist
.append(kids
)
268 self
.dad
= self
.objlist
[-1]
269 self
.objlist
[-1].kids
= kids
271 def parse_name(self
, value
):
272 name
= value
.split('"')[1]
273 self
.objlist
[-1].name
= name
275 def parse_data(self
, value
):
276 data
= self
.lines
[self
.i
].strip()
277 self
.objlist
[-1].data
= data
279 def parse_tex(self
, value
):
280 line
= self
.lines
[self
.i
- 1] # parse again to properly get paths with spaces
281 texture
= line
.split('"')[1]
282 self
.objlist
[-1].tex
= texture
284 def parse_texrep(self
, trash
):
285 trep
= self
.lines
[self
.i
- 1]
287 trep
= [float(trep
[1]), float(trep
[2])]
288 self
.objlist
[-1].texrep
= trep
289 self
.objlist
[-1].texoff
= [0, 0]
291 def parse_texoff(self
, trash
):
292 toff
= self
.lines
[self
.i
- 1]
294 toff
= [float(toff
[1]), float(toff
[2])]
295 self
.objlist
[-1].texoff
= toff
297 def parse_mat(self
, value
):
300 line
= lines
[i
].split()
302 mat_col
= mat_amb
= mat_emit
= mat_spec_col
= [0,0,0]
306 while line
[0] == 'MATERIAL':
307 mat_name
= line
[1].split('"')[1]
308 mat_col
= map(float,[line
[3],line
[4],line
[5]])
309 v
= map(float,[line
[7],line
[8],line
[9]])
310 mat_amb
= (v
[0]+v
[1]+v
[2]) / 3.0
311 v
= map(float,[line
[11],line
[12],line
[13]])
312 mat_emit
= (v
[0]+v
[1]+v
[2]) / 3.0
313 mat_spec_col
= map(float,[line
[15],line
[16],line
[17]])
314 mat_spec
= float(line
[19]) / 64.0
315 mat_alpha
= float(line
[-1])
316 mat_alpha
= 1 - mat_alpha
317 self
.mlist
.append([mat_name
, mat_col
, mat_amb
, mat_emit
, mat_spec_col
, mat_spec
, mat_alpha
])
319 line
= lines
[i
].split()
323 def parse_rot(self
, trash
):
325 ob
= self
.objlist
[-1]
326 rot
= self
.lines
[i
].split(' ', 1)[1]
327 rot
= map(float, rot
.split())
328 matrix
= Matrix(rot
[:3], rot
[3:6], rot
[6:])
330 size
= matrix
.scalePart() # vector
333 def parse_loc(self
, trash
):
335 loc
= self
.lines
[i
].split(' ', 1)[1]
336 loc
= map(float, loc
.split())
337 self
.objlist
[-1].loc
= Vector(loc
)
339 def parse_crease(self
, value
):
340 # AC3D: range is [0.0, 180.0]; Blender: [1, 80]
342 self
.objlist
[-1].crease
= int(value
)
344 def parse_subdiv(self
, value
):
345 self
.objlist
[-1].subdiv
= int(value
)
347 def parse_vert(self
, value
):
350 obj
= self
.objlist
[-1]
355 line
= lines
[i
].split()
356 line
= map(float, line
)
361 if vlist
: # prepend a vertex at 1st position to deal with vindex 0 issues
362 vlist
.insert(0, line
)
366 def parse_surf(self
, value
):
371 obj
= self
.objlist
[-1]
373 matlist
= obj
.matlist
377 badface_notpoly
= badface_multirefs
= 0
380 flags
= lines
[i
].split()[1][2:]
382 flaghigh
= int(flags
[0])
383 flaglow
= int(flags
[1])
386 flaglow
= int(flags
[0])
388 is_smooth
= flaghigh
& 1
389 twoside
= flaghigh
& 2
390 nextline
= lines
[i
+1].split()
391 if nextline
[0] != 'mat': # the "mat" line may be missing (found in one buggy .ac file)
393 if not matid
in matlist
: matlist
.append(matid
)
396 matid
= int(nextline
[1])
397 if not matid
in matlist
: matlist
.append(matid
)
398 nextline
= lines
[i
+2].split()
400 refs
= int(nextline
[1])
409 line
= lines
[i
].split()
410 v
= int(line
[0]) + 1 # + 1 to avoid vindex == 0
411 uv
= [float(line
[1]), float(line
[2])]
413 fuv
.append(Vector(uv
))
417 if flaglow
: # it's a line or closed line, not a polygon
418 while len(face
) >= 2:
423 if flaglow
== 1 and edges
: # closed line
424 face
= [edges
[-1][-1], edges
[0][0]]
429 # check for bad face, that references same vertex more than once
432 # less than 3 vertices, not a face
434 elif sum(map(face
.count
, face
)) != lenface
:
435 # multiple references to the same vertex
436 badface_multirefs
+= 1
437 else: # ok, seems fine
438 if len(face
) > 4: # ngon, triangulate it
441 polyline
.append(Vector(vlist
[vi
]))
442 tris
= PolyFill([polyline
])
444 tri
= [face
[t
[0]], face
[t
[1]], face
[t
[2]]]
445 triuvs
= [fuv
[t
[0]], fuv
[t
[1]], fuv
[t
[2]]]
452 obj
.flist_cfg
.extend([[matid
, is_smooth
, twoside
]] * len(faces
))
453 obj
.flist_v
.extend(faces
)
454 obj
.flist_uv
.extend(fuvs
)
455 obj
.elist
.extend(edges
) # loose edges
459 if badface_notpoly
or badface_multirefs
:
460 inform('Object "%s" - ignoring bad faces:' % obj
.name
)
462 inform('\t%d face(s) with less than 3 vertices.' % badface_notpoly
)
463 if badface_multirefs
:
464 inform('\t%d face(s) with multiple references to a same vertex.' % badface_multirefs
)
468 def parse_file(self
):
471 line
= lines
[i
].split()
475 for k
in self
.token
.keys():
482 result
= self
.token
[kw
](line
[1])
484 break # bad .ac file, stop parsing
486 line
= lines
[i
].split()
488 # for each group of meshes we try to find one that can be used as
489 # parent of the group in Blender.
490 # If not found, we can use an Empty as parent.
491 def found_parent(self
, groupname
, olist
):
492 l
= [o
for o
in olist
if o
.type == AC_POLY \
493 and not o
.kids
and not o
.rot
and not o
.loc
]
496 if o
.name
== groupname
:
501 def build_hierarchy(self
):
502 blmatrix
= AC_TO_BLEND_MATRIX
504 olist
= self
.objlist
[1:]
514 children
= newlist
[-kids
:]
515 newlist
= newlist
[:-kids
]
516 if o
.type == AC_GROUP
:
517 parent
= self
.found_parent(o
.name
, children
)
519 children
.remove(parent
)
520 o
.bl_obj
= parent
.bl_obj
521 else: # not found, use an empty
522 empty
= scene
.objects
.new('Empty', o
.name
)
525 bl_children
= [c
.bl_obj
for c
in children
if c
.bl_obj
!= None]
527 o
.bl_obj
.makeParent(bl_children
, 0, 1)
528 for child
in children
:
530 if not blob
: continue
532 eul
= euler_in_radians(child
.rot
.toEuler())
535 blob
.size
= child
.size
537 child
.loc
= Vector(0.0, 0.0, 0.0)
538 blob
.setLocation(child
.loc
)
542 for o
in newlist
: # newlist now only has objs w/o parents
547 o
.bl_obj
.size
= o
.size
549 blob
.setEuler([1.5707963267948966, 0, 0])
551 matrix
= o
.rot
* blmatrix
552 eul
= euler_in_radians(matrix
.toEuler())
557 o
.loc
= Vector(0.0, 0.0, 0.0)
558 blob
.setLocation(o
.loc
) # forces DAG update, so we do it even for 0, 0, 0
560 # XXX important: until we fix the BPy API so it doesn't increase user count
561 # when wrapping a Blender object, this piece of code is needed for proper
562 # object (+ obdata) deletion in Blender:
563 for o
in self
.objlist
:
567 def testAC3DImport(self
):
569 FACE_TWOSIDE
= Mesh
.FaceModes
['TWOSIDE']
570 FACE_TEX
= Mesh
.FaceModes
['TEX']
571 MESH_AUTOSMOOTH
= Mesh
.Modes
['AUTOSMOOTH']
573 MAT_MODE_ZTRANSP
= Material
.Modes
['ZTRANSP']
574 MAT_MODE_TRANSPSHADOW
= Material
.Modes
['TRANSPSHADOW']
578 bl_images
= {} # loaded texture images
579 missing_textures
= [] # textures we couldn't find
581 objlist
= self
.objlist
[1:] # skip 'world'
584 has_transp_mats
= False
585 for mat
in self
.mlist
:
587 m
= Material
.New(name
)
588 m
.rgbCol
= (mat
[1][0], mat
[1][1], mat
[1][2])
591 m
.specCol
= (mat
[4][0], mat
[4][1], mat
[4][2])
595 m
.mode |
= MAT_MODE_ZTRANSP
596 has_transp_mats
= True
601 mat
.mode |
= MAT_MODE_TRANSPSHADOW
603 obj_idx
= 0 # index of current obj in loop
605 if obj
.type == AC_GROUP
:
607 elif obj
.type == AC_LIGHT
:
608 light
= Lamp
.New('Lamp')
609 object = scene
.objects
.new(light
, obj
.name
)
613 light
.name
= obj
.data
618 # old .ac files used empty meshes as groups, convert to a real ac group
619 if not obj
.vlist
and obj
.kids
:
624 object = scene
.objects
.new(mesh
, obj
.name
)
627 if obj
.data
: mesh
.name
= obj
.data
628 mesh
.degr
= obj
.crease
# will auto clamp to [1, 80]
630 if not obj
.vlist
: # no vertices? nothing more to do
633 mesh
.verts
.extend(obj
.vlist
)
637 if bmat
.index(mat
) in obj
.matlist
:
638 objmat_indices
.append(bmat
.index(mat
))
639 mesh
.materials
+= [mat
]
640 if DISPLAY_TRANSP
and mat
.alpha
< 1.0:
647 mesh
.faces
.extend(obj
.flist_v
)
649 facesnum
= len(mesh
.faces
)
651 if facesnum
== 0: # shouldn't happen, of course
656 # checking if the .ac file had duplicate faces (Blender ignores them)
657 if facesnum
!= len(obj
.flist_v
):
658 # it has, ugh. Let's clean the uv list:
659 lenfl
= len(obj
.flist_v
)
661 uvlist
= obj
.flist_uv
662 cfglist
= obj
.flist_cfg
666 while fi
> 0: # remove data related to duplicates
668 if flist
[fi
] in flist
[:fi
]:
674 if obj
.tex
in bl_images
.keys():
675 img
= bl_images
[obj
.tex
]
676 elif obj
.tex
not in missing_textures
:
679 baseimgname
= bsys
.basename(objtex
)
680 if bsys
.exists(objtex
) == 1:
682 elif bsys
.exists(bsys
.join(self
.importdir
, objtex
)):
683 texfname
= bsys
.join(self
.importdir
, objtex
)
685 if baseimgname
.find('\\') > 0:
686 baseimgname
= bsys
.basename(objtex
.replace('\\','/'))
687 objtex
= bsys
.join(self
.importdir
, baseimgname
)
688 if bsys
.exists(objtex
) == 1:
691 objtex
= bsys
.join(TEXTURES_DIR
, baseimgname
)
692 if bsys
.exists(objtex
):
696 img
= Image
.Load(texfname
)
697 # Commented because it's unnecessary:
698 #img.xrep = int(obj.texrep[0])
699 #img.yrep = int(obj.texrep[1])
701 bl_images
[obj
.tex
] = img
703 inform("Couldn't load texture: %s" % baseimgname
)
705 missing_textures
.append(obj
.tex
)
706 inform("Couldn't find texture: %s" % baseimgname
)
708 for i
in range(facesnum
):
713 bface
= mesh
.faces
[i
]
714 bface
.smooth
= is_smooth
715 if twoside
: bface
.mode |
= FACE_TWOSIDE
717 bface
.mode |
= FACE_TEX
719 bface
.mat
= objmat_indices
.index(fmat
)
720 fuv
= obj
.flist_uv
[i
]
732 mesh
.faces
[i
].uv
= fuv
734 # finally, delete the 1st vertex we added to prevent vindices == 0
739 mesh
.mode
= MESH_AUTOSMOOTH
741 # subdiv: create SUBSURF modifier in Blender
742 if SUBDIV
and obj
.subdiv
> 0:
744 subdiv_render
= subdiv
746 if subdiv_render
> 6: subdiv_render
= 6
747 if subdiv
> 3: subdiv
= 3
748 modif
= object.modifiers
.append(Modifier
.Types
.SUBSURF
)
749 modif
[Modifier
.Settings
.LEVELS
] = subdiv
750 modif
[Modifier
.Settings
.RENDLEVELS
] = subdiv_render
754 self
.build_hierarchy()
757 # End of class AC3DImport
759 def filesel_callback(filename
):
761 inform("\nTrying to import AC3D model(s) from:\n%s ..." % filename
)
763 starttime
= bsys
.time()
764 test
= AC3DImport(filename
)
766 endtime
= bsys
.time() - starttime
767 inform('Done! Data imported in %.3f seconds.\n' % endtime
)
771 Window
.FileSelector(filesel_callback
, "Import AC3D", "*.ac")