3
Copyright (C) 2007 John Beard john.j.beard@gmail.com
5
##This extension draws 3d objects from a Wavefront .obj 3D file stored in a local folder
6
##Many settings for appearance, lighting, rotation, etc are available.
10
# __--``| |_--``| __--
11
# __--`` | __--``| |_--``
13
# | <----|--------|-----_0-----|----------------
15
# | __--`` <-``| |_--``
20
#Vertices are given as "v" followed by three numbers (x,y,z).
21
#All files need a vertex list
24
#Faces are given by a list of vertices
25
#(vertex 1 is the first in the list above, 2 the second, etc):
28
#Edges are given by a list of vertices. These will be broken down
29
#into adjacent pairs automatically.
32
#Faces are rendered according to the painter's algorithm and perhaps
33
#back-face culling, if selected. The parameter to sort the faces by
34
#is user-selectable between max, min and average z-value of the vertices
37
This program is free software; you can redistribute it and/or modify
38
it under the terms of the GNU General Public License as published by
39
the Free Software Foundation; either version 2 of the License, or
40
(at your option) any later version.
42
This program is distributed in the hope that it will be useful,
43
but WITHOUT ANY WARRANTY; without even the implied warranty of
44
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
45
GNU General Public License for more details.
47
You should have received a copy of the GNU General Public License
48
along with this program; if not, write to the Free Software
49
Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
53
import simplestyle, sys, re
58
inkex.errormsg(_("Failed to import the numpy module. This module is required by this extension. Please install them and try again. On a Debian-like system this can be done with the command, sudo apt-get install python-numpy."))
62
def get_filename(self_options):
63
if self_options.obj == 'from_file':
64
file = self_options.spec_file
66
file = self_options.obj + '.obj'
72
if __name__ == '__main__':
73
filename = sys.argv[0]
76
path = os.path.abspath(os.path.dirname(filename))
77
path = os.path.join(path, 'Poly3DObjects', name)
80
def get_obj_data(obj, name):
81
infile = open(objfile(name))
84
getname = '(.[nN]ame:\\s*)(.*)'
85
floating = '([\-\+\\d*\.e]*)' #a possibly non-integer number, with +/- and exponent.
86
getvertex = '(v\\s+)'+floating+'\\s+'+floating+'\\s+'+floating
87
getedgeline = '(l\\s+)(.*)'
88
getfaceline = '(f\\s+)(.*)'
89
getnextint = '(\\d+)([/\\d]*)(.*)'#we need to deal with 123\343\123 or 123\\456 as equivalent to 123 (we are ignoring the other options in the obj file)
92
if line[0]=='#': #we have a comment line
93
m = re.search(getname, line) #check to see if this line contains a name
95
obj.name = m.group(2) #if it does, set the property
96
elif line[0] == 'v': #we have a vertex (maybe)
97
m = re.search(getvertex, line) #check to see if this line contains a valid vertex
98
if m: #we have a valid vertex
99
obj.vtx.append( [float(m.group(2)), float(m.group(3)), float(m.group(4)) ] )
100
elif line[0] == 'l': #we have a line (maybe)
101
m = re.search(getedgeline, line) #check to see if this line begins 'l '
102
if m: #we have a line beginning 'l '
105
m2 = re.search(getnextint, line)
107
vtxlist.append( int(m2.group(1)) )
108
line = m2.group(3)#remainder
111
if len(vtxlist) > 1:#we need at least 2 vertices to make an edge
112
for i in range (len(vtxlist)-1):#we can have more than one vertex per line - get adjacent pairs
113
obj.edg.append( ( vtxlist[i], vtxlist[i+1] ) )#get the vertex pair between that vertex and the next
114
elif line[0] == 'f': #we have a face (maybe)
115
m = re.search(getfaceline, line)
116
if m: #we have a line beginning 'f '
119
m2 = re.search(getnextint, line)
121
vtxlist.append( int(m2.group(1)) )
122
line = m2.group(3)#remainder
125
if len(vtxlist) > 2: #we need at least 3 vertices to make an edge
126
obj.fce.append(vtxlist)
128
if obj.name == '':#no name was found, use filename, without extension (.obj)
129
obj.name = name[0:-4]
131
#RENDERING AND SVG OUTPUT FUNCTIONS
133
def draw_SVG_dot((cx, cy), st, name, parent):
134
style = { 'stroke': '#000000', 'stroke-width':str(st.th), 'fill': st.fill, 'stroke-opacity':st.s_opac, 'fill-opacity':st.f_opac}
135
circ_attribs = {'style':simplestyle.formatStyle(style),
136
inkex.addNS('label','inkscape'):name,
138
'cx':str(cx), 'cy':str(-cy)}
139
inkex.etree.SubElement(parent, inkex.addNS('circle','svg'), circ_attribs )
141
def draw_SVG_line((x1, y1),(x2, y2), st, name, parent):
142
style = { 'stroke': '#000000', 'stroke-width':str(st.th), 'stroke-linecap':st.linecap}
143
line_attribs = {'style':simplestyle.formatStyle(style),
144
inkex.addNS('label','inkscape'):name,
145
'd':'M '+str(x1)+','+str(-y1)+' L '+str(x2)+','+str(-y2)}
146
inkex.etree.SubElement(parent, inkex.addNS('path','svg'), line_attribs )
148
def draw_SVG_poly(pts, face, st, name, parent):
149
style = { 'stroke': '#000000', 'stroke-width':str(st.th), 'stroke-linejoin':st.linejoin, \
150
'stroke-opacity':st.s_opac, 'fill': st.fill, 'fill-opacity':st.f_opac}
151
for i in range(len(face)):
152
if i == 0:#for first point
156
d = d+ str(pts[face[i]-1][0]) + ',' + str(-pts[face[i]-1][1])#add point
157
d = d + 'z' #close the polygon
159
line_attribs = {'style':simplestyle.formatStyle(style),
160
inkex.addNS('label','inkscape'):name,'d': d}
161
inkex.etree.SubElement(parent, inkex.addNS('path','svg'), line_attribs )
163
def draw_edges( edge_list, pts, st, parent ):
164
for edge in edge_list:#for every edge
165
pt_1 = pts[ edge[0]-1 ][0:2] #the point at the start
166
pt_2 = pts[ edge[1]-1 ][0:2] #the point at the end
167
name = 'Edge'+str(edge[0])+'-'+str(edge[1])
168
draw_SVG_line(pt_1,pt_2,st, name, parent)#plot edges
170
def draw_faces( faces_data, pts, obj, shading, fill_col,st, parent):
171
for face in faces_data:#for every polygon that has been sorted
173
st.fill = get_darkened_colour(fill_col, face[1]/pi)#darken proportionally to angle to lighting vector
175
st.fill = get_darkened_colour(fill_col, 1)#do not darken colour
177
face_no = face[3]#the number of the face to draw
178
draw_SVG_poly(pts, obj.fce[ face_no ], st, 'Face:'+str(face_no), parent)
180
def get_darkened_colour( (r,g,b), factor):
181
#return a hex triplet of colour, reduced in lightness proportionally to a value between 0 and 1
182
return '#' + "%02X" % floor( factor*r ) \
183
+ "%02X" % floor( factor*g ) \
184
+ "%02X" % floor( factor*b ) #make the colour string
186
def make_rotation_log(options):
187
#makes a string recording the axes and angles of each roation, so an object can be repeated
188
return options.r1_ax+str('%.2f'%options.r1_ang)+':'+\
189
options.r2_ax+str('%.2f'%options.r2_ang)+':'+\
190
options.r3_ax+str('%.2f'%options.r3_ang)+':'+\
191
options.r1_ax+str('%.2f'%options.r4_ang)+':'+\
192
options.r2_ax+str('%.2f'%options.r5_ang)+':'+\
193
options.r3_ax+str('%.2f'%options.r6_ang)
195
#MATHEMATICAL FUNCTIONS
196
def get_angle( vector1, vector2 ): #returns the angle between two vectors
197
return acos( dot(vector1, vector2) )
199
def length(vector):#return the pythagorean length of a vector
200
return sqrt(dot(vector,vector))
202
def normalise(vector):#return the unit vector pointing in the same direction as the argument
203
return vector / length(vector)
205
def get_normal( pts, face): #returns the normal vector for the plane passing though the first three elements of face of pts
206
#n = pt[0]->pt[1] x pt[0]->pt[3]
207
a = (array(pts[ face[0]-1 ]) - array(pts[ face[1]-1 ]))
208
b = (array(pts[ face[0]-1 ]) - array(pts[ face[2]-1 ]))
209
return cross(a,b).flatten()
211
def get_unit_normal(pts, face, cw_wound): #returns the unit normal for the plane passing through the first three points of face, taking account of winding
213
winding = -1 #if it is clockwise wound, reverse the vecotr direction
215
winding = 1 #else leave alone
217
return winding*normalise(get_normal(pts, face))
219
def rotate( matrix, angle, axis ):#choose the correct rotation matrix to use
221
matrix = rot_x(matrix, angle)
223
matrix = rot_y(matrix, angle)
225
matrix = rot_z(matrix, angle)
228
def rot_z( matrix , a):#rotate around the z-axis by a radians
229
trans_mat = mat(array( [[ cos(a) , -sin(a) , 0 ],
230
[ sin(a) , cos(a) , 0 ],
232
return trans_mat*matrix
234
def rot_y( matrix , a):#rotate around the y-axis by a radians
235
trans_mat = mat(array( [[ cos(a) , 0 , sin(a) ],
237
[-sin(a) , 0 , cos(a) ]]))
238
return trans_mat*matrix
240
def rot_x( matrix , a):#rotate around the x-axis by a radians
241
trans_mat = mat(array( [[ 1 , 0 , 0 ],
242
[ 0 , cos(a) ,-sin(a) ],
243
[ 0 , sin(a) , cos(a) ]]))
244
return trans_mat*matrix
246
def get_transformed_pts( vtx_list, trans_mat):#translate the points according to the matrix
249
transformed_pts.append((trans_mat * mat(vtx).T).T.tolist()[0] )#transform the points at add to the list
250
return transformed_pts
252
def get_max_z(pts, face): #returns the largest z_value of any point in the face
253
max_z = pts[ face[0]-1 ][2]
254
for i in range(1, len(face)):
255
if pts[ face[0]-1 ][2] >= max_z:
256
max_z = pts[ face[0]-1 ][2]
259
def get_min_z(pts, face): #returns the smallest z_value of any point in the face
260
min_z = pts[ face[0]-1 ][2]
261
for i in range(1, len(face)):
262
if pts[ face[i]-1 ][2] <= min_z:
263
min_z = pts[ face[i]-1 ][2]
266
def get_cent_z(pts, face): #returns the centroid z_value of any point in the face
268
for i in range(len(face)):
269
sum += pts[ face[i]-1 ][2]
272
def get_z_sort_param(pts, face, method): #returns the z-sorting parameter specified by 'method' ('max', 'min', 'cent')
275
z_sort_param = get_max_z(pts, face)
276
elif method == 'min':
277
z_sort_param = get_min_z(pts, face)
279
z_sort_param = get_cent_z(pts, face)
282
#OBJ DATA MANIPULATION
283
def remove_duplicates(list):#removes the duplicates from a list
284
list.sort()#sort the list
287
for i in range(len(list)-2, -1, -1):
294
def make_edge_list(face_list):#make an edge vertex list from an existing face vertex list
296
for i in range(len(face_list)):#for every face
297
edges = len(face_list[i]) #number of edges around that face
298
for j in range(edges):#for every vertex in that face
299
new_edge = [face_list[i][j], face_list[i][(j+1)%edges] ]
300
new_edge.sort() #put in ascending order of vertices (to ensure we spot duplicates)
301
edge_list.append( new_edge )#get the vertex pair between that vertex and the next
303
return remove_duplicates(edge_list)
305
class Style(object): #container for style information
306
def __init__(self,options):
311
self.f_opac = str(options.f_opac/100.0)
312
self.s_opac = str(options.s_opac/100.0)
313
self.linecap = 'round'
314
self.linejoin = 'round'
316
class Obj(object): #a 3d object defined by the vertices and the faces (eg a polyhedron)
317
#edges can be generated from this information
324
def set_type(self, options):
325
if options.type == 'face':
329
inkex.errormsg(_('No face data found in specified file\n'))
335
inkex.errormsg(_('No edge data found in specified file\n'))
338
class Poly_3D(inkex.Effect):
340
inkex.Effect.__init__(self)
341
self.OptionParser.add_option("--tab",
342
action="store", type="string",
343
dest="tab", default="object")
346
self.OptionParser.add_option("--obj",
347
action="store", type="string",
348
dest="obj", default='cube')
349
self.OptionParser.add_option("--spec_file",
350
action="store", type="string",
351
dest="spec_file", default='great_rhombicuboct.obj')
352
self.OptionParser.add_option("--cw_wound",
353
action="store", type="inkbool",
354
dest="cw_wound", default='true')
355
self.OptionParser.add_option("--type",
356
action="store", type="string",
357
dest="type", default='face')
359
self.OptionParser.add_option("--r1_ax",
360
action="store", type="string",
361
dest="r1_ax", default=0)
362
self.OptionParser.add_option("--r2_ax",
363
action="store", type="string",
364
dest="r2_ax", default=0)
365
self.OptionParser.add_option("--r3_ax",
366
action="store", type="string",
367
dest="r3_ax", default=0)
368
self.OptionParser.add_option("--r4_ax",
369
action="store", type="string",
370
dest="r4_ax", default=0)
371
self.OptionParser.add_option("--r5_ax",
372
action="store", type="string",
373
dest="r5_ax", default=0)
374
self.OptionParser.add_option("--r6_ax",
375
action="store", type="string",
376
dest="r6_ax", default=0)
377
self.OptionParser.add_option("--r1_ang",
378
action="store", type="float",
379
dest="r1_ang", default=0)
380
self.OptionParser.add_option("--r2_ang",
381
action="store", type="float",
382
dest="r2_ang", default=0)
383
self.OptionParser.add_option("--r3_ang",
384
action="store", type="float",
385
dest="r3_ang", default=0)
386
self.OptionParser.add_option("--r4_ang",
387
action="store", type="float",
388
dest="r4_ang", default=0)
389
self.OptionParser.add_option("--r5_ang",
390
action="store", type="float",
391
dest="r5_ang", default=0)
392
self.OptionParser.add_option("--r6_ang",
393
action="store", type="float",
394
dest="r6_ang", default=0)
395
self.OptionParser.add_option("--scl",
396
action="store", type="float",
397
dest="scl", default=100.0)
399
self.OptionParser.add_option("--show",
400
action="store", type="string",
401
dest="show", default='faces')
402
self.OptionParser.add_option("--shade",
403
action="store", type="inkbool",
404
dest="shade", default='true')
405
self.OptionParser.add_option("--f_r",
406
action="store", type="int",
407
dest="f_r", default=255)
408
self.OptionParser.add_option("--f_g",
409
action="store", type="int",
410
dest="f_g", default=0)
411
self.OptionParser.add_option("--f_b",
412
action="store", type="int",
413
dest="f_b", default=0)
414
self.OptionParser.add_option("--f_opac",
415
action="store", type="int",
416
dest="f_opac", default=100)
417
self.OptionParser.add_option("--s_opac",
418
action="store", type="int",
419
dest="s_opac", default=100)
420
self.OptionParser.add_option("--th",
421
action="store", type="float",
422
dest="th", default=2)
423
self.OptionParser.add_option("--lv_x",
424
action="store", type="float",
425
dest="lv_x", default=1)
426
self.OptionParser.add_option("--lv_y",
427
action="store", type="float",
428
dest="lv_y", default=1)
429
self.OptionParser.add_option("--lv_z",
430
action="store", type="float",
431
dest="lv_z", default=-2)
432
self.OptionParser.add_option("--back",
433
action="store", type="inkbool",
434
dest="back", default='false')
435
self.OptionParser.add_option("--norm",
436
action="store", type="inkbool",
437
dest="norm", default='true')
438
self.OptionParser.add_option("--z_sort",
439
action="store", type="string",
440
dest="z_sort", default='min')
444
so = self.options#shorthand
446
#INITIALISE AND LOAD DATA
448
obj = Obj() #create the object
449
file = get_filename(so)#get the file to load data from
450
get_obj_data(obj, file)#load data from the obj file
451
obj.set_type(so)#set the type (face or edge) as per the settings
453
st = Style(so) #initialise style
454
fill_col = (so.f_r, so.f_g, so.f_b) #colour tuple for the face fill
455
lighting = normalise( (so.lv_x,-so.lv_y,so.lv_z) ) #unit light vector
457
#INKSCAPE GROUP TO CONTAIN THE POLYHEDRON
459
#Put in in the centre of the current view
460
poly_transform = 'translate(' + str( self.view_center[0]) + ',' + str( self.view_center[1]) + ')'
461
#we will put all the rotations in the object name, so it can be repeated in
462
poly_name = obj.name+':'+make_rotation_log(so)
463
poly_attribs = {inkex.addNS('label','inkscape'):poly_name,
464
'transform':poly_transform }
465
poly = inkex.etree.SubElement(self.current_layer, 'g', poly_attribs)#the group to put everything in
467
#TRANFORMATION OF THE OBJECT (ROTATION, SCALE, ETC)
469
trans_mat = mat(identity(3, float)) #init. trans matrix as identity matrix
470
for i in range(1, 7):#for each rotation
471
axis = eval('so.r'+str(i)+'_ax')
472
angle = eval('so.r'+str(i)+'_ang') *pi/180
473
trans_mat = rotate(trans_mat, angle, axis)
474
trans_mat = trans_mat*so.scl #scale by linear factor (do this only after the transforms to reduce round-off)
476
transformed_pts = get_transformed_pts(obj.vtx, trans_mat) #the points as projected in the z-axis onto the viewplane
478
#RENDERING OF THE OBJECT
481
for i in range(len(transformed_pts)):
482
draw_SVG_dot([transformed_pts[i][0],transformed_pts[i][1]], st, 'Point'+str(i), poly)#plot points using transformed_pts x and y coords
484
elif so.show == 'edg':
485
if obj.type == 'face':#we must generate the edge list from the faces
486
edge_list = make_edge_list(obj.fce)
487
else:#we already have an edge list
490
draw_edges( edge_list, transformed_pts, st, poly)
492
elif so.show == 'fce':
493
if obj.type == 'face':#we have a face list
497
for i in range(len(obj.fce)):
498
face = obj.fce[i] #the face we are dealing with
499
norm = get_unit_normal(transformed_pts, face, so.cw_wound) #get the normal vector to the face
500
angle = get_angle( norm, lighting )#get the angle between the normal and the lighting vector
501
z_sort_param = get_z_sort_param(transformed_pts, face, so.z_sort)
503
if so.back or norm[2] > 0: # include all polygons or just the front-facing ones as needed
504
z_list.append((z_sort_param, angle, norm, i))#record the maximum z-value of the face and angle to light, along with the face ID and normal
506
z_list.sort(lambda x, y: cmp(x[0],y[0])) #sort by ascending sort parameter of the face
507
draw_faces( z_list, transformed_pts, obj, so.shade, fill_col, st, poly)
509
else:#we cannot generate a list of faces from the edges without a lot of computation
510
inkex.errormsg(_('Face Data Not Found. Ensure file contains face data, and check the file is imported as "Face-Specified" under the "Model File" tab.\n'))
512
inkex.errormsg(_('Internal Error. No view type selected\n'))
514
if __name__ == '__main__':
519
# vim: expandtab shiftwidth=4 tabstop=8 softtabstop=4 encoding=utf-8 textwidth=99