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
21import os
22
23import bpy
24from mathutils import Matrix, Vector, Color
25from bpy_extras import io_utils, node_shader_utils
26
27from bpy_extras.wm_utils.progress_report import (
28    ProgressReport,
29    ProgressReportSubstep,
30)
31
32
33def name_compat(name):
34    if name is None:
35        return 'None'
36    else:
37        return name.replace(' ', '_')
38
39
40def 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
49def 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
160def 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
171def 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
237def 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
655def _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"""
740Currently the exporter lacks these features:
741* multiple scene export (only active scene is written)
742* particles
743"""
744
745
746def 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'}
793