Sun position: T80379 Startup breaks add-on: no World and UI warning
[blender-addons.git] / io_scene_obj / export_obj.py
1 # ##### BEGIN GPL LICENSE BLOCK #####
2 #
3 #  This program is free software; you can redistribute it and/or
4 #  modify it under the terms of the GNU General Public License
5 #  as published by the Free Software Foundation; either version 2
6 #  of the License, or (at your option) any later version.
7 #
8 #  This program is distributed in the hope that it will be useful,
9 #  but WITHOUT ANY WARRANTY; without even the implied warranty of
10 #  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
11 #  GNU General Public License for more details.
12 #
13 #  You should have received a copy of the GNU General Public License
14 #  along with this program; if not, write to the Free Software Foundation,
15 #  Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
16 #
17 # ##### END GPL LICENSE BLOCK #####
18
19 # <pep8 compliant>
20
21 import os
22
23 import bpy
24 from mathutils import Matrix, Vector, Color
25 from bpy_extras import io_utils, node_shader_utils
26
27 from bpy_extras.wm_utils.progress_report import (
28     ProgressReport,
29     ProgressReportSubstep,
30 )
31
32
33 def name_compat(name):
34     if name is None:
35         return 'None'
36     else:
37         return name.replace(' ', '_')
38
39
40 def mesh_triangulate(me):
41     import bmesh
42     bm = bmesh.new()
43     bm.from_mesh(me)
44     bmesh.ops.triangulate(bm, faces=bm.faces)
45     bm.to_mesh(me)
46     bm.free()
47
48
49 def write_mtl(scene, filepath, path_mode, copy_set, mtl_dict):
50     source_dir = os.path.dirname(bpy.data.filepath)
51     dest_dir = os.path.dirname(filepath)
52
53     with open(filepath, "w", encoding="utf8", newline="\n") as f:
54         fw = f.write
55
56         fw('# Blender MTL File: %r\n' % (os.path.basename(bpy.data.filepath) or "None"))
57         fw('# Material Count: %i\n' % len(mtl_dict))
58
59         mtl_dict_values = list(mtl_dict.values())
60         mtl_dict_values.sort(key=lambda m: m[0])
61
62         # Write material/image combinations we have used.
63         # Using mtl_dict.values() directly gives un-predictable order.
64         for mtl_mat_name, mat in mtl_dict_values:
65             # Get the Blender data for the material and the image.
66             # Having an image named None will make a bug, dont do it :)
67
68             fw('\nnewmtl %s\n' % mtl_mat_name)  # Define a new material: matname_imgname
69
70             mat_wrap = node_shader_utils.PrincipledBSDFWrapper(mat) if mat else None
71
72             if mat_wrap:
73                 use_mirror = mat_wrap.metallic != 0.0
74                 use_transparency = mat_wrap.alpha != 1.0
75
76                 # XXX Totally empirical conversion, trying to adapt it
77                 #     (from 1.0 - 0.0 Principled BSDF range to 0.0 - 900.0 OBJ specular exponent range)...
78                 spec = (1.0 - mat_wrap.roughness) * 30
79                 spec *= spec
80                 fw('Ns %.6f\n' % spec)
81
82                 # Ambient
83                 if use_mirror:
84                     fw('Ka %.6f %.6f %.6f\n' % (mat_wrap.metallic, mat_wrap.metallic, mat_wrap.metallic))
85                 else:
86                     fw('Ka %.6f %.6f %.6f\n' % (1.0, 1.0, 1.0))
87                 fw('Kd %.6f %.6f %.6f\n' % mat_wrap.base_color[:3])  # Diffuse
88                 # XXX TODO Find a way to handle tint and diffuse color, in a consistent way with import...
89                 fw('Ks %.6f %.6f %.6f\n' % (mat_wrap.specular, mat_wrap.specular, mat_wrap.specular))  # Specular
90                 # Emission, not in original MTL standard but seems pretty common, see T45766.
91                 emission_strength = mat_wrap.emission_strength
92                 emission = [emission_strength * c for c in mat_wrap.emission_color[:3]]
93                 fw('Ke %.6f %.6f %.6f\n' % tuple(emission))
94                 fw('Ni %.6f\n' % mat_wrap.ior)  # Refraction index
95                 fw('d %.6f\n' % mat_wrap.alpha)  # Alpha (obj uses 'd' for dissolve)
96
97                 # See http://en.wikipedia.org/wiki/Wavefront_.obj_file for whole list of values...
98                 # Note that mapping is rather fuzzy sometimes, trying to do our best here.
99                 if mat_wrap.specular == 0:
100                     fw('illum 1\n')  # no specular.
101                 elif use_mirror:
102                     if use_transparency:
103                         fw('illum 6\n')  # Reflection, Transparency, Ray trace
104                     else:
105                         fw('illum 3\n')  # Reflection and Ray trace
106                 elif use_transparency:
107                     fw('illum 9\n')  # 'Glass' transparency and no Ray trace reflection... fuzzy matching, but...
108                 else:
109                     fw('illum 2\n')  # light normally
110
111                 #### And now, the image textures...
112                 image_map = {
113                         "map_Kd": "base_color_texture",
114                         "map_Ka": None,  # ambient...
115                         "map_Ks": "specular_texture",
116                         "map_Ns": "roughness_texture",
117                         "map_d": "alpha_texture",
118                         "map_Tr": None,  # transmission roughness?
119                         "map_Bump": "normalmap_texture",
120                         "disp": None,  # displacement...
121                         "refl": "metallic_texture",
122                         "map_Ke": "emission_color_texture" if emission_strength != 0.0 else None,
123                         }
124
125                 for key, mat_wrap_key in sorted(image_map.items()):
126                     if mat_wrap_key is None:
127                         continue
128                     tex_wrap = getattr(mat_wrap, mat_wrap_key, None)
129                     if tex_wrap is None:
130                         continue
131                     image = tex_wrap.image
132                     if image is None:
133                         continue
134
135                     filepath = io_utils.path_reference(image.filepath, source_dir, dest_dir,
136                                                        path_mode, "", copy_set, image.library)
137                     options = []
138                     if key == "map_Bump":
139                         if mat_wrap.normalmap_strength != 1.0:
140                             options.append('-bm %.6f' % mat_wrap.normalmap_strength)
141                     if tex_wrap.translation != Vector((0.0, 0.0, 0.0)):
142                         options.append('-o %.6f %.6f %.6f' % tex_wrap.translation[:])
143                     if tex_wrap.scale != Vector((1.0, 1.0, 1.0)):
144                         options.append('-s %.6f %.6f %.6f' % tex_wrap.scale[:])
145                     if options:
146                         fw('%s %s %s\n' % (key, " ".join(options), repr(filepath)[1:-1]))
147                     else:
148                         fw('%s %s\n' % (key, repr(filepath)[1:-1]))
149
150             else:
151                 # Write a dummy material here?
152                 fw('Ns 500\n')
153                 fw('Ka 0.8 0.8 0.8\n')
154                 fw('Kd 0.8 0.8 0.8\n')
155                 fw('Ks 0.8 0.8 0.8\n')
156                 fw('d 1\n')  # No alpha
157                 fw('illum 2\n')  # light normally
158
159
160 def test_nurbs_compat(ob):
161     if ob.type != 'CURVE':
162         return False
163
164     for nu in ob.data.splines:
165         if nu.point_count_v == 1 and nu.type != 'BEZIER':  # not a surface and not bezier
166             return True
167
168     return False
169
170
171 def write_nurb(fw, ob, ob_mat):
172     tot_verts = 0
173     cu = ob.data
174
175     # use negative indices
176     for nu in cu.splines:
177         if nu.type == 'POLY':
178             DEG_ORDER_U = 1
179         else:
180             DEG_ORDER_U = nu.order_u - 1  # odd but tested to be correct
181
182         if nu.type == 'BEZIER':
183             print("\tWarning, bezier curve:", ob.name, "only poly and nurbs curves supported")
184             continue
185
186         if nu.point_count_v > 1:
187             print("\tWarning, surface:", ob.name, "only poly and nurbs curves supported")
188             continue
189
190         if len(nu.points) <= DEG_ORDER_U:
191             print("\tWarning, order_u is lower then vert count, skipping:", ob.name)
192             continue
193
194         pt_num = 0
195         do_closed = nu.use_cyclic_u
196         do_endpoints = (do_closed == 0) and nu.use_endpoint_u
197
198         for pt in nu.points:
199             fw('v %.6f %.6f %.6f\n' % (ob_mat @ pt.co.to_3d())[:])
200             pt_num += 1
201         tot_verts += pt_num
202
203         fw('g %s\n' % (name_compat(ob.name)))  # name_compat(ob.getData(1)) could use the data name too
204         fw('cstype bspline\n')  # not ideal, hard coded
205         fw('deg %d\n' % DEG_ORDER_U)  # not used for curves but most files have it still
206
207         curve_ls = [-(i + 1) for i in range(pt_num)]
208
209         # 'curv' keyword
210         if do_closed:
211             if DEG_ORDER_U == 1:
212                 pt_num += 1
213                 curve_ls.append(-1)
214             else:
215                 pt_num += DEG_ORDER_U
216                 curve_ls = curve_ls + curve_ls[0:DEG_ORDER_U]
217
218         fw('curv 0.0 1.0 %s\n' % (" ".join([str(i) for i in curve_ls])))  # Blender has no U and V values for the curve
219
220         # 'parm' keyword
221         tot_parm = (DEG_ORDER_U + 1) + pt_num
222         tot_parm_div = float(tot_parm - 1)
223         parm_ls = [(i / tot_parm_div) for i in range(tot_parm)]
224
225         if do_endpoints:  # end points, force param
226             for i in range(DEG_ORDER_U + 1):
227                 parm_ls[i] = 0.0
228                 parm_ls[-(1 + i)] = 1.0
229
230         fw("parm u %s\n" % " ".join(["%.6f" % i for i in parm_ls]))
231
232         fw('end\n')
233
234     return tot_verts
235
236
237 def write_file(filepath, objects, depsgraph, scene,
238                EXPORT_TRI=False,
239                EXPORT_EDGES=False,
240                EXPORT_SMOOTH_GROUPS=False,
241                EXPORT_SMOOTH_GROUPS_BITFLAGS=False,
242                EXPORT_NORMALS=False,
243                EXPORT_UV=True,
244                EXPORT_MTL=True,
245                EXPORT_APPLY_MODIFIERS=True,
246                EXPORT_APPLY_MODIFIERS_RENDER=False,
247                EXPORT_BLEN_OBS=True,
248                EXPORT_GROUP_BY_OB=False,
249                EXPORT_GROUP_BY_MAT=False,
250                EXPORT_KEEP_VERT_ORDER=False,
251                EXPORT_POLYGROUPS=False,
252                EXPORT_CURVE_AS_NURBS=True,
253                EXPORT_GLOBAL_MATRIX=None,
254                EXPORT_PATH_MODE='AUTO',
255                progress=ProgressReport(),
256                ):
257     """
258     Basic write function. The context and options must be already set
259     This can be accessed externaly
260     eg.
261     write( 'c:\\test\\foobar.obj', Blender.Object.GetSelected() ) # Using default options.
262     """
263     if EXPORT_GLOBAL_MATRIX is None:
264         EXPORT_GLOBAL_MATRIX = Matrix()
265
266     def veckey3d(v):
267         return round(v.x, 4), round(v.y, 4), round(v.z, 4)
268
269     def veckey2d(v):
270         return round(v[0], 4), round(v[1], 4)
271
272     def findVertexGroupName(face, vWeightMap):
273         """
274         Searches the vertexDict to see what groups is assigned to a given face.
275         We use a frequency system in order to sort out the name because a given vertex can
276         belong to two or more groups at the same time. To find the right name for the face
277         we list all the possible vertex group names with their frequency and then sort by
278         frequency in descend order. The top element is the one shared by the highest number
279         of vertices is the face's group
280         """
281         weightDict = {}
282         for vert_index in face.vertices:
283             vWeights = vWeightMap[vert_index]
284             for vGroupName, weight in vWeights:
285                 weightDict[vGroupName] = weightDict.get(vGroupName, 0.0) + weight
286
287         if weightDict:
288             return max((weight, vGroupName) for vGroupName, weight in weightDict.items())[1]
289         else:
290             return '(null)'
291
292     with ProgressReportSubstep(progress, 2, "OBJ Export path: %r" % filepath, "OBJ Export Finished") as subprogress1:
293         with open(filepath, "w", encoding="utf8", newline="\n") as f:
294             fw = f.write
295
296             # Write Header
297             fw('# Blender v%s OBJ File: %r\n' % (bpy.app.version_string, os.path.basename(bpy.data.filepath)))
298             fw('# www.blender.org\n')
299
300             # Tell the obj file what material file to use.
301             if EXPORT_MTL:
302                 mtlfilepath = os.path.splitext(filepath)[0] + ".mtl"
303                 # filepath can contain non utf8 chars, use repr
304                 fw('mtllib %s\n' % repr(os.path.basename(mtlfilepath))[1:-1])
305
306             # Initialize totals, these are updated each object
307             totverts = totuvco = totno = 1
308
309             face_vert_index = 1
310
311             # A Dict of Materials
312             # (material.name, image.name):matname_imagename # matname_imagename has gaps removed.
313             mtl_dict = {}
314             # Used to reduce the usage of matname_texname materials, which can become annoying in case of
315             # repeated exports/imports, yet keeping unique mat names per keys!
316             # mtl_name: (material.name, image.name)
317             mtl_rev_dict = {}
318
319             copy_set = set()
320
321             # Get all meshes
322             subprogress1.enter_substeps(len(objects))
323             for i, ob_main in enumerate(objects):
324                 # ignore dupli children
325                 if ob_main.parent and ob_main.parent.instance_type in {'VERTS', 'FACES'}:
326                     subprogress1.step("Ignoring %s, dupli child..." % ob_main.name)
327                     continue
328
329                 obs = [(ob_main, ob_main.matrix_world)]
330                 if ob_main.is_instancer:
331                     obs += [(dup.instance_object.original, dup.matrix_world.copy())
332                             for dup in depsgraph.object_instances
333                             if dup.parent and dup.parent.original == ob_main]
334                     # ~ print(ob_main.name, 'has', len(obs) - 1, 'dupli children')
335
336                 subprogress1.enter_substeps(len(obs))
337                 for ob, ob_mat in obs:
338                     with ProgressReportSubstep(subprogress1, 6) as subprogress2:
339                         uv_unique_count = no_unique_count = 0
340
341                         # Nurbs curve support
342                         if EXPORT_CURVE_AS_NURBS and test_nurbs_compat(ob):
343                             ob_mat = EXPORT_GLOBAL_MATRIX @ ob_mat
344                             totverts += write_nurb(fw, ob, ob_mat)
345                             continue
346                         # END NURBS
347
348                         ob_for_convert = ob.evaluated_get(depsgraph) if EXPORT_APPLY_MODIFIERS else ob.original
349
350                         try:
351                             me = ob_for_convert.to_mesh()
352                         except RuntimeError:
353                             me = None
354
355                         if me is None:
356                             continue
357
358                         # _must_ do this before applying transformation, else tessellation may differ
359                         if EXPORT_TRI:
360                             # _must_ do this first since it re-allocs arrays
361                             mesh_triangulate(me)
362
363                         me.transform(EXPORT_GLOBAL_MATRIX @ ob_mat)
364                         # If negative scaling, we have to invert the normals...
365                         if ob_mat.determinant() < 0.0:
366                             me.flip_normals()
367
368                         if EXPORT_UV:
369                             faceuv = len(me.uv_layers) > 0
370                             if faceuv:
371                                 uv_layer = me.uv_layers.active.data[:]
372                         else:
373                             faceuv = False
374
375                         me_verts = me.vertices[:]
376
377                         # Make our own list so it can be sorted to reduce context switching
378                         face_index_pairs = [(face, index) for index, face in enumerate(me.polygons)]
379
380                         if EXPORT_EDGES:
381                             edges = me.edges
382                         else:
383                             edges = []
384
385                         if not (len(face_index_pairs) + len(edges) + len(me.vertices)):  # Make sure there is something to write
386                             # clean up
387                             ob_for_convert.to_mesh_clear()
388                             continue  # dont bother with this mesh.
389
390                         if EXPORT_NORMALS and face_index_pairs:
391                             me.calc_normals_split()
392                             # No need to call me.free_normals_split later, as this mesh is deleted anyway!
393
394                         loops = me.loops
395
396                         if (EXPORT_SMOOTH_GROUPS or EXPORT_SMOOTH_GROUPS_BITFLAGS) and face_index_pairs:
397                             smooth_groups, smooth_groups_tot = me.calc_smooth_groups(use_bitflags=EXPORT_SMOOTH_GROUPS_BITFLAGS)
398                             if smooth_groups_tot <= 1:
399                                 smooth_groups, smooth_groups_tot = (), 0
400                         else:
401                             smooth_groups, smooth_groups_tot = (), 0
402
403                         materials = me.materials[:]
404                         material_names = [m.name if m else None for m in materials]
405
406                         # avoid bad index errors
407                         if not materials:
408                             materials = [None]
409                             material_names = [name_compat(None)]
410
411                         # Sort by Material, then images
412                         # so we dont over context switch in the obj file.
413                         if EXPORT_KEEP_VERT_ORDER:
414                             pass
415                         else:
416                             if len(materials) > 1:
417                                 if smooth_groups:
418                                     sort_func = lambda a: (a[0].material_index,
419                                                            smooth_groups[a[1]] if a[0].use_smooth else False)
420                                 else:
421                                     sort_func = lambda a: (a[0].material_index,
422                                                            a[0].use_smooth)
423                             else:
424                                 # no materials
425                                 if smooth_groups:
426                                     sort_func = lambda a: smooth_groups[a[1] if a[0].use_smooth else False]
427                                 else:
428                                     sort_func = lambda a: a[0].use_smooth
429
430                             face_index_pairs.sort(key=sort_func)
431
432                             del sort_func
433
434                         # Set the default mat to no material and no image.
435                         contextMat = 0, 0  # Can never be this, so we will label a new material the first chance we get.
436                         contextSmooth = None  # Will either be true or false,  set bad to force initialization switch.
437
438                         if EXPORT_BLEN_OBS or EXPORT_GROUP_BY_OB:
439                             name1 = ob.name
440                             name2 = ob.data.name
441                             if name1 == name2:
442                                 obnamestring = name_compat(name1)
443                             else:
444                                 obnamestring = '%s_%s' % (name_compat(name1), name_compat(name2))
445
446                             if EXPORT_BLEN_OBS:
447                                 fw('o %s\n' % obnamestring)  # Write Object name
448                             else:  # if EXPORT_GROUP_BY_OB:
449                                 fw('g %s\n' % obnamestring)
450
451                         subprogress2.step()
452
453                         # Vert
454                         for v in me_verts:
455                             fw('v %.6f %.6f %.6f\n' % v.co[:])
456
457                         subprogress2.step()
458
459                         # UV
460                         if faceuv:
461                             # in case removing some of these dont get defined.
462                             uv = f_index = uv_index = uv_key = uv_val = uv_ls = None
463
464                             uv_face_mapping = [None] * len(face_index_pairs)
465
466                             uv_dict = {}
467                             uv_get = uv_dict.get
468                             for f, f_index in face_index_pairs:
469                                 uv_ls = uv_face_mapping[f_index] = []
470                                 for uv_index, l_index in enumerate(f.loop_indices):
471                                     uv = uv_layer[l_index].uv
472                                     # include the vertex index in the key so we don't share UV's between vertices,
473                                     # allowed by the OBJ spec but can cause issues for other importers, see: T47010.
474
475                                     # this works too, shared UV's for all verts
476                                     #~ uv_key = veckey2d(uv)
477                                     uv_key = loops[l_index].vertex_index, veckey2d(uv)
478
479                                     uv_val = uv_get(uv_key)
480                                     if uv_val is None:
481                                         uv_val = uv_dict[uv_key] = uv_unique_count
482                                         fw('vt %.6f %.6f\n' % uv[:])
483                                         uv_unique_count += 1
484                                     uv_ls.append(uv_val)
485
486                             del uv_dict, uv, f_index, uv_index, uv_ls, uv_get, uv_key, uv_val
487                             # Only need uv_unique_count and uv_face_mapping
488
489                         subprogress2.step()
490
491                         # NORMAL, Smooth/Non smoothed.
492                         if EXPORT_NORMALS:
493                             no_key = no_val = None
494                             normals_to_idx = {}
495                             no_get = normals_to_idx.get
496                             loops_to_normals = [0] * len(loops)
497                             for f, f_index in face_index_pairs:
498                                 for l_idx in f.loop_indices:
499                                     no_key = veckey3d(loops[l_idx].normal)
500                                     no_val = no_get(no_key)
501                                     if no_val is None:
502                                         no_val = normals_to_idx[no_key] = no_unique_count
503                                         fw('vn %.4f %.4f %.4f\n' % no_key)
504                                         no_unique_count += 1
505                                     loops_to_normals[l_idx] = no_val
506                             del normals_to_idx, no_get, no_key, no_val
507                         else:
508                             loops_to_normals = []
509
510                         subprogress2.step()
511
512                         # XXX
513                         if EXPORT_POLYGROUPS:
514                             # Retrieve the list of vertex groups
515                             vertGroupNames = ob.vertex_groups.keys()
516                             if vertGroupNames:
517                                 currentVGroup = ''
518                                 # Create a dictionary keyed by face id and listing, for each vertex, the vertex groups it belongs to
519                                 vgroupsMap = [[] for _i in range(len(me_verts))]
520                                 for v_idx, v_ls in enumerate(vgroupsMap):
521                                     v_ls[:] = [(vertGroupNames[g.group], g.weight) for g in me_verts[v_idx].groups]
522
523                         for f, f_index in face_index_pairs:
524                             f_smooth = f.use_smooth
525                             if f_smooth and smooth_groups:
526                                 f_smooth = smooth_groups[f_index]
527                             f_mat = min(f.material_index, len(materials) - 1)
528
529                             # MAKE KEY
530                             key = material_names[f_mat], None  # No image, use None instead.
531
532                             # Write the vertex group
533                             if EXPORT_POLYGROUPS:
534                                 if vertGroupNames:
535                                     # find what vertext group the face belongs to
536                                     vgroup_of_face = findVertexGroupName(f, vgroupsMap)
537                                     if vgroup_of_face != currentVGroup:
538                                         currentVGroup = vgroup_of_face
539                                         fw('g %s\n' % vgroup_of_face)
540
541                             # CHECK FOR CONTEXT SWITCH
542                             if key == contextMat:
543                                 pass  # Context already switched, dont do anything
544                             else:
545                                 if key[0] is None and key[1] is None:
546                                     # Write a null material, since we know the context has changed.
547                                     if EXPORT_GROUP_BY_MAT:
548                                         # can be mat_image or (null)
549                                         fw("g %s_%s\n" % (name_compat(ob.name), name_compat(ob.data.name)))
550                                     if EXPORT_MTL:
551                                         fw("usemtl (null)\n")  # mat, image
552
553                                 else:
554                                     mat_data = mtl_dict.get(key)
555                                     if not mat_data:
556                                         # First add to global dict so we can export to mtl
557                                         # Then write mtl
558
559                                         # Make a new names from the mat and image name,
560                                         # converting any spaces to underscores with name_compat.
561
562                                         # If none image dont bother adding it to the name
563                                         # Try to avoid as much as possible adding texname (or other things)
564                                         # to the mtl name (see [#32102])...
565                                         mtl_name = "%s" % name_compat(key[0])
566                                         if mtl_rev_dict.get(mtl_name, None) not in {key, None}:
567                                             if key[1] is None:
568                                                 tmp_ext = "_NONE"
569                                             else:
570                                                 tmp_ext = "_%s" % name_compat(key[1])
571                                             i = 0
572                                             while mtl_rev_dict.get(mtl_name + tmp_ext, None) not in {key, None}:
573                                                 i += 1
574                                                 tmp_ext = "_%3d" % i
575                                             mtl_name += tmp_ext
576                                         mat_data = mtl_dict[key] = mtl_name, materials[f_mat]
577                                         mtl_rev_dict[mtl_name] = key
578
579                                     if EXPORT_GROUP_BY_MAT:
580                                         # can be mat_image or (null)
581                                         fw("g %s_%s_%s\n" % (name_compat(ob.name), name_compat(ob.data.name), mat_data[0]))
582                                     if EXPORT_MTL:
583                                         fw("usemtl %s\n" % mat_data[0])  # can be mat_image or (null)
584
585                             contextMat = key
586                             if f_smooth != contextSmooth:
587                                 if f_smooth:  # on now off
588                                     if smooth_groups:
589                                         f_smooth = smooth_groups[f_index]
590                                         fw('s %d\n' % f_smooth)
591                                     else:
592                                         fw('s 1\n')
593                                 else:  # was off now on
594                                     fw('s off\n')
595                                 contextSmooth = f_smooth
596
597                             f_v = [(vi, me_verts[v_idx], l_idx)
598                                    for vi, (v_idx, l_idx) in enumerate(zip(f.vertices, f.loop_indices))]
599
600                             fw('f')
601                             if faceuv:
602                                 if EXPORT_NORMALS:
603                                     for vi, v, li in f_v:
604                                         fw(" %d/%d/%d" % (totverts + v.index,
605                                                           totuvco + uv_face_mapping[f_index][vi],
606                                                           totno + loops_to_normals[li],
607                                                           ))  # vert, uv, normal
608                                 else:  # No Normals
609                                     for vi, v, li in f_v:
610                                         fw(" %d/%d" % (totverts + v.index,
611                                                        totuvco + uv_face_mapping[f_index][vi],
612                                                        ))  # vert, uv
613
614                                 face_vert_index += len(f_v)
615
616                             else:  # No UV's
617                                 if EXPORT_NORMALS:
618                                     for vi, v, li in f_v:
619                                         fw(" %d//%d" % (totverts + v.index, totno + loops_to_normals[li]))
620                                 else:  # No Normals
621                                     for vi, v, li in f_v:
622                                         fw(" %d" % (totverts + v.index))
623
624                             fw('\n')
625
626                         subprogress2.step()
627
628                         # Write edges.
629                         if EXPORT_EDGES:
630                             for ed in edges:
631                                 if ed.is_loose:
632                                     fw('l %d %d\n' % (totverts + ed.vertices[0], totverts + ed.vertices[1]))
633
634                         # Make the indices global rather then per mesh
635                         totverts += len(me_verts)
636                         totuvco += uv_unique_count
637                         totno += no_unique_count
638
639                         # clean up
640                         ob_for_convert.to_mesh_clear()
641
642                 subprogress1.leave_substeps("Finished writing geometry of '%s'." % ob_main.name)
643             subprogress1.leave_substeps()
644
645         subprogress1.step("Finished exporting geometry, now exporting materials")
646
647         # Now we have all our materials, save them
648         if EXPORT_MTL:
649             write_mtl(scene, mtlfilepath, EXPORT_PATH_MODE, copy_set, mtl_dict)
650
651         # copy all collected files.
652         io_utils.path_reference_copy(copy_set)
653
654
655 def _write(context, filepath,
656            EXPORT_TRI,  # ok
657            EXPORT_EDGES,
658            EXPORT_SMOOTH_GROUPS,
659            EXPORT_SMOOTH_GROUPS_BITFLAGS,
660            EXPORT_NORMALS,  # ok
661            EXPORT_UV,  # ok
662            EXPORT_MTL,
663            EXPORT_APPLY_MODIFIERS,  # ok
664            EXPORT_APPLY_MODIFIERS_RENDER,  # ok
665            EXPORT_BLEN_OBS,
666            EXPORT_GROUP_BY_OB,
667            EXPORT_GROUP_BY_MAT,
668            EXPORT_KEEP_VERT_ORDER,
669            EXPORT_POLYGROUPS,
670            EXPORT_CURVE_AS_NURBS,
671            EXPORT_SEL_ONLY,  # ok
672            EXPORT_ANIMATION,
673            EXPORT_GLOBAL_MATRIX,
674            EXPORT_PATH_MODE,  # Not used
675            ):
676
677     with ProgressReport(context.window_manager) as progress:
678         base_name, ext = os.path.splitext(filepath)
679         context_name = [base_name, '', '', ext]  # Base name, scene name, frame number, extension
680
681         depsgraph = context.evaluated_depsgraph_get()
682         scene = context.scene
683
684         # Exit edit mode before exporting, so current object states are exported properly.
685         if bpy.ops.object.mode_set.poll():
686             bpy.ops.object.mode_set(mode='OBJECT')
687
688         orig_frame = scene.frame_current
689
690         # Export an animation?
691         if EXPORT_ANIMATION:
692             scene_frames = range(scene.frame_start, scene.frame_end + 1)  # Up to and including the end frame.
693         else:
694             scene_frames = [orig_frame]  # Dont export an animation.
695
696         # Loop through all frames in the scene and export.
697         progress.enter_substeps(len(scene_frames))
698         for frame in scene_frames:
699             if EXPORT_ANIMATION:  # Add frame to the filepath.
700                 context_name[2] = '_%.6d' % frame
701
702             scene.frame_set(frame, subframe=0.0)
703             if EXPORT_SEL_ONLY:
704                 objects = context.selected_objects
705             else:
706                 objects = scene.objects
707
708             full_path = ''.join(context_name)
709
710             # erm... bit of a problem here, this can overwrite files when exporting frames. not too bad.
711             # EXPORT THE FILE.
712             progress.enter_substeps(1)
713             write_file(full_path, objects, depsgraph, scene,
714                        EXPORT_TRI,
715                        EXPORT_EDGES,
716                        EXPORT_SMOOTH_GROUPS,
717                        EXPORT_SMOOTH_GROUPS_BITFLAGS,
718                        EXPORT_NORMALS,
719                        EXPORT_UV,
720                        EXPORT_MTL,
721                        EXPORT_APPLY_MODIFIERS,
722                        EXPORT_APPLY_MODIFIERS_RENDER,
723                        EXPORT_BLEN_OBS,
724                        EXPORT_GROUP_BY_OB,
725                        EXPORT_GROUP_BY_MAT,
726                        EXPORT_KEEP_VERT_ORDER,
727                        EXPORT_POLYGROUPS,
728                        EXPORT_CURVE_AS_NURBS,
729                        EXPORT_GLOBAL_MATRIX,
730                        EXPORT_PATH_MODE,
731                        progress,
732                        )
733             progress.leave_substeps()
734
735         scene.frame_set(orig_frame, subframe=0.0)
736         progress.leave_substeps()
737
738
739 """
740 Currently the exporter lacks these features:
741 * multiple scene export (only active scene is written)
742 * particles
743 """
744
745
746 def save(context,
747          filepath,
748          *,
749          use_triangles=False,
750          use_edges=True,
751          use_normals=False,
752          use_smooth_groups=False,
753          use_smooth_groups_bitflags=False,
754          use_uvs=True,
755          use_materials=True,
756          use_mesh_modifiers=True,
757          use_mesh_modifiers_render=False,
758          use_blen_objects=True,
759          group_by_object=False,
760          group_by_material=False,
761          keep_vertex_order=False,
762          use_vertex_groups=False,
763          use_nurbs=True,
764          use_selection=True,
765          use_animation=False,
766          global_matrix=None,
767          path_mode='AUTO'
768          ):
769
770     _write(context, filepath,
771            EXPORT_TRI=use_triangles,
772            EXPORT_EDGES=use_edges,
773            EXPORT_SMOOTH_GROUPS=use_smooth_groups,
774            EXPORT_SMOOTH_GROUPS_BITFLAGS=use_smooth_groups_bitflags,
775            EXPORT_NORMALS=use_normals,
776            EXPORT_UV=use_uvs,
777            EXPORT_MTL=use_materials,
778            EXPORT_APPLY_MODIFIERS=use_mesh_modifiers,
779            EXPORT_APPLY_MODIFIERS_RENDER=use_mesh_modifiers_render,
780            EXPORT_BLEN_OBS=use_blen_objects,
781            EXPORT_GROUP_BY_OB=group_by_object,
782            EXPORT_GROUP_BY_MAT=group_by_material,
783            EXPORT_KEEP_VERT_ORDER=keep_vertex_order,
784            EXPORT_POLYGROUPS=use_vertex_groups,
785            EXPORT_CURVE_AS_NURBS=use_nurbs,
786            EXPORT_SEL_ONLY=use_selection,
787            EXPORT_ANIMATION=use_animation,
788            EXPORT_GLOBAL_MATRIX=global_matrix,
789            EXPORT_PATH_MODE=path_mode,
790            )
791
792     return {'FINISHED'}