addons-contrib: object select_set syntax update
[blender-addons-contrib.git] / io_scene_map / export_map.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 #http://www.pasteall.org/47943/python
20
21 # <pep8-80 compliant>
22
23 import bpy
24 import os
25 import mathutils
26 from mathutils import Vector
27
28 from contextlib import redirect_stdout
29 import io
30 stdout = io.StringIO()
31
32 # TODO, make options
33 PREF_SCALE = 1
34 PREF_FACE_THICK = 0.1
35 PREF_GRID_SNAP = False
36 # Quake 1/2?
37 # Quake 3+?
38 PREF_DEF_TEX_OPTS = '0 0 0 1 1 0 0 0'  # not user settable yet
39
40 PREF_NULL_TEX = 'NULL'  # not user settable yet
41 PREF_INVIS_TEX = 'common/caulk'
42 PREF_DOOM3_FORMAT = True
43
44
45 def face_uv_image_get(me, face):
46     uv_faces = me.uv_textures.active
47     if uv_faces:
48         return uv_faces.data[face.index].image
49     else:
50         return None
51
52
53 def face_uv_coords_get(me, face):
54     tf_uv_faces = me.tessface_uv_textures.active
55     if tf_uv_faces:
56         return tf_uv_faces.data[face.index].uv_raw[:]
57     else:
58         return None
59
60
61 def face_material_get(me, face):
62     idx = face.material_index
63     return me.materials[idx] if idx < len(me.materials) else None
64
65
66 def poly_to_doom(me, p, radius):
67     """
68     Convert a face into Doom3 representation (infinite plane defined by its normal
69     and distance from origin along that normal).
70     """
71     # Compute the distance to the mesh from the origin to the plane.
72     # Line from origin in the direction of the face normal.
73     origin = Vector((0, 0, 0))
74     target = Vector(p.normal) * radius
75     # Find the target point.
76     intersect = mathutils.geometry.intersect_line_plane(origin, target, Vector(p.center), Vector(p.normal))
77     # We have to handle cases where intersection with face happens on the "negative" part of the vector!
78     length = intersect.length
79     nor = p.normal.copy()
80     if (nor.dot(intersect.normalized()) > 0):
81         length *= -1
82     nor.resize_4d()
83     nor.w = length
84     return nor
85
86
87 def doom_are_same_planes(p1, p2):
88     """
89     To avoid writing two planes that are nearly the same!
90     """
91     # XXX Is sign of the normal/length important in Doom for plane definition??? For now, assume that no!
92     if p1.w < 0:
93         p1 = p1 * -1.0
94     if p2.w < 0:
95         p2 = p2 * -1.0
96
97     threshold = 0.0001
98
99     if abs(p1.w - p2.w) > threshold:
100         return False
101
102     # Distances are the same, check orientations!
103     if p1.xyz.normalized().dot(p2.xyz.normalized()) < (1 - threshold):
104         return False
105
106     # Same plane!
107     return True
108
109
110 def doom_check_plane(done_planes, plane):
111     """
112     Check if plane as already been handled, or is similar enough to an already handled one.
113     Return True if it has already been handled somehow.
114     done_planes is expected to be a dict {written_plane: {written_plane, similar_plane_1, similar_plane_2, ...}, ...}.
115     """
116     p_key = tuple(plane)
117     if p_key in done_planes:
118         return True
119     for p, dp in done_planes.items():
120         if p_key in dp:
121             return True
122         elif doom_are_same_planes(Vector(p), plane):
123             done_planes[p].add(p_key)
124             return True
125     done_planes[p_key] = {p_key}
126     return False
127
128
129 def ob_to_radius(ob):
130     radius = max(Vector(pt).length for pt in ob.bound_box)
131
132     # Make the ray casts, go just outside the bounding sphere.
133     return radius * 1.1
134
135
136 def is_cube_facegroup(faces):
137     """
138     Returns a bool, true if the faces make up a cube
139     """
140     # cube must have 6 faces
141     if len(faces) != 6:
142         # print('1')
143         return False
144
145     # Check for quads and that there are 6 unique verts
146     verts = {}
147     for f in faces:
148         f_v = f.vertices[:]
149         if len(f_v) != 4:
150             return False
151
152         for v in f_v:
153             verts[v] = 0
154
155     if len(verts) != 8:
156         return False
157
158     # Now check that each vert has 3 face users
159     for f in faces:
160         f_v = f.vertices[:]
161         for v in f_v:
162             verts[v] += 1
163
164     for v in verts.values():
165         if v != 3:  # vert has 3 users?
166             return False
167
168     # Could we check for 12 unique edges??, probably not needed.
169     return True
170
171
172 def is_tricyl_facegroup(faces):
173     """
174     is the face group a tri cylinder
175     Returns a bool, true if the faces make an extruded tri solid
176     """
177
178     # tricyl must have 5 faces
179     if len(faces) != 5:
180         #  print('1')
181         return False
182
183     # Check for quads and that there are 6 unique verts
184     verts = {}
185     tottri = 0
186     for f in faces:
187         if len(f.vertices) == 3:
188             tottri += 1
189
190         for vi in f.vertices:
191             verts[vi] = 0
192
193     if len(verts) != 6 or tottri != 2:
194         return False
195
196     # Now check that each vert has 3 face users
197     for f in faces:
198         for vi in f.vertices:
199             verts[vi] += 1
200
201     for v in verts.values():
202         if v != 3:  # vert has 3 users?
203             return False
204
205     # Could we check for 9 unique edges??, probably not needed.
206     return True
207
208
209 def split_mesh_in_convex_parts(me):
210     """
211     Not implemented yet. Should split given mesh into manifold convex meshes.
212     For now simply always returns the given mesh.
213     """
214     # TODO.
215     return (me,)
216
217
218 def round_vec(v):
219     if PREF_GRID_SNAP:
220         return v.to_tuple(0)
221     else:
222         return v[:]
223
224
225 def write_quake_brush_cube(fw, ob, faces):
226     """
227     Takes 6 faces and writes a brush,
228     these faces can be from 1 mesh, 1 cube within a mesh of larger cubes
229     Faces could even come from different meshes or be contrived.
230     """
231     format_vec = '( %d %d %d ) ' if PREF_GRID_SNAP else '( %.9g %.9g %.9g ) '
232
233     fw('// brush from cube\n{\n')
234
235     for f in faces:
236         # from 4 verts this gets them in reversed order and only 3 of them
237         # 0,1,2,3 -> 2,1,0
238         me = f.id_data  # XXX25
239
240         for v in f.vertices[:][2::-1]:
241             fw(format_vec % round_vec(me.vertices[v].co))
242
243         material = face_material_get(me, f)
244
245         if material and material.game_settings.invisible:
246             fw(PREF_INVIS_TEX)
247         else:
248             image = face_uv_image_get(me, f)
249             if image:
250                 fw(os.path.splitext(bpy.path.basename(image.filepath))[0])
251             else:
252                 fw(PREF_NULL_TEX)
253         fw(" %s\n" % PREF_DEF_TEX_OPTS)  # Texture stuff ignored for now
254
255     fw('}\n')
256
257
258 def write_quake_brush_face(fw, ob, face):
259     """
260     takes a face and writes it as a brush
261     each face is a cube/brush.
262     """
263     format_vec = '( %d %d %d ) ' if PREF_GRID_SNAP else '( %.9g %.9g %.9g ) '
264
265     image_text = PREF_NULL_TEX
266
267     me = face.id_data
268     material = face_material_get(me, face)
269
270     if material and material.game_settings.invisible:
271         image_text = PREF_INVIS_TEX
272     else:
273         image = face_uv_image_get(me, face)
274         if image:
275             image_text = os.path.splitext(bpy.path.basename(image.filepath))[0]
276
277     # reuse face vertices
278     f_vertices = [me.vertices[vi] for vi in face.vertices]
279
280     # original verts as tuples for writing
281     orig_vco = tuple(round_vec(v.co) for v in f_vertices)
282
283     # new verts that give the face a thickness
284     dist = PREF_SCALE * PREF_FACE_THICK
285     new_vco = tuple(round_vec(v.co - (v.normal * dist)) for v in f_vertices)
286     #new_vco = [round_vec(v.co - (face.no * dist)) for v in face]
287
288     fw('// brush from face\n{\n')
289     # front
290     for co in orig_vco[2::-1]:
291         fw(format_vec % co)
292
293     fw(image_text)
294     fw(" %s\n" % PREF_DEF_TEX_OPTS)  # Texture stuff ignored for now
295
296     for co in new_vco[:3]:
297         fw(format_vec % co)
298     if image and not material.game_settings.use_backface_culling: #uf.use_twoside:
299         fw(image_text)
300     else:
301         fw(PREF_INVIS_TEX)
302     fw(" %s\n" % PREF_DEF_TEX_OPTS)  # Texture stuff ignored for now
303
304     # sides.
305     if len(orig_vco) == 3:  # Tri, it seemms tri brushes are supported.
306         index_pairs = ((0, 1), (1, 2), (2, 0))
307     else:
308         index_pairs = ((0, 1), (1, 2), (2, 3), (3, 0))
309
310     for i1, i2 in index_pairs:
311         for co in orig_vco[i1], orig_vco[i2], new_vco[i2]:
312             fw(format_vec % co)
313         fw(PREF_INVIS_TEX)
314         fw(" %s\n" % PREF_DEF_TEX_OPTS)  # Texture stuff ignored for now
315
316     fw('}\n')
317
318
319 def write_doom_brush(fw, ob, me):
320     """
321     Takes a mesh object and writes its convex parts.
322     """
323     format_vec = '( {} {} {} {} ) '
324     format_vec_uv = '( ( {} {} {} ) ( {} {} {} ) ) '
325     # Get the bounding sphere for the object for ray-casting
326     radius = ob_to_radius(ob)
327
328     fw('// brush from faces\n{\n'
329        'brushDef3\n{\n'
330       )
331
332     done_planes = {}  # Store already written plane, to avoid writing the same one (or a similar-enough one) again.
333
334     for p in me.polygons:
335         image_text = PREF_NULL_TEX
336         material = face_material_get(me, p)
337
338         if material:
339             if material.game_settings.invisible:
340                 image_text = PREF_INVIS_TEX
341             else:
342                 image_text = material.name
343
344         # reuse face vertices
345         plane = poly_to_doom(me, p, radius)
346         if plane is None:
347             print("    ERROR: Could not create the plane from polygon!");
348         elif doom_check_plane(done_planes, plane):
349             #print("    WARNING: Polygon too similar to another one!");
350             pass
351         else:
352             fw(format_vec.format(*plane.to_tuple(6)))
353             fw(format_vec_uv.format(0.015625, 0, 1, 0, 0.015625, 1)) # TODO insert UV stuff here
354             fw('"%s" ' % image_text)
355             fw("%s\n" % PREF_DEF_TEX_OPTS)  # Texture stuff ignored for now
356
357     fw('}\n}\n')
358
359
360 def write_node_map(fw, ob):
361     """
362     Writes the properties of an object (empty in this case)
363     as a MAP node as long as it has the property name - classname
364     returns True/False based on weather a node was written
365     """
366     props = [(p.name, p.value) for p in ob.game.properties]
367
368     IS_MAP_NODE = False
369     for name, value in props:
370         if name == "classname":
371             IS_MAP_NODE = True
372             break
373
374     if not IS_MAP_NODE:
375         return False
376
377     # Write a node
378     fw('{\n')
379     for name_value in props:
380         fw('"%s" "%s"\n' % name_value)
381     fw('"origin" "%.9g %.9g %.9g"\n' % round_vec(ob.matrix_world.to_translation()))
382
383     fw('}\n')
384     return True
385
386
387 def split_objects(context, objects):
388     scene = context.scene
389     final_objects = []
390
391     bpy.ops.object.select_all(action='DESELECT')
392     for ob in objects:
393         ob.select_set(True)
394
395     bpy.ops.object.duplicate()
396     objects = bpy.context.selected_objects
397
398     bpy.ops.object.select_all(action='DESELECT')
399
400     tot_ob = len(objects)
401     for i, ob in enumerate(objects):
402         print("Splitting object: %d/%d" % (i, tot_ob))
403         ob.select_set(True)
404
405         if ob.type == "MESH":
406             scene.objects.active = ob
407             bpy.ops.object.mode_set(mode='EDIT')
408             bpy.ops.mesh.select_all(action='DESELECT')
409             bpy.ops.mesh.select_mode(type='EDGE')
410             bpy.ops.object.mode_set(mode='OBJECT')
411             for edge in ob.data.edges:
412                 if edge.use_seam:
413                     edge.select = True
414             bpy.ops.object.mode_set(mode='EDIT')
415             bpy.ops.mesh.edge_split()
416             bpy.ops.mesh.separate(type='LOOSE')
417             bpy.ops.object.mode_set(mode='OBJECT')
418
419             split_objects = context.selected_objects
420             for split_ob in split_objects:
421                 assert(split_ob.type == "MESH")
422
423                 scene.objects.active = split_ob
424                 bpy.ops.object.mode_set(mode='EDIT')
425                 bpy.ops.mesh.select_mode(type='EDGE')
426                 bpy.ops.mesh.select_all(action="SELECT")
427                 bpy.ops.mesh.region_to_loop()
428                 bpy.ops.mesh.fill_holes(sides=8)
429                 slot_idx = 0
430                 for slot_idx, m in enumerate(split_ob.material_slots):
431                    if m.name == "textures/common/caulk":
432                       break
433                    #if m.name != "textures/common/caulk":
434                    #   mat = bpy.data.materials.new("textures/common/caulk")
435                    #   bpy.context.object.data.materials.append(mat)
436                 split_ob.active_material_index = slot_idx  # we need to use either actual material name or custom property instead of index
437                 bpy.ops.object.material_slot_assign()
438                 with redirect_stdout(stdout):
439                    bpy.ops.mesh.remove_doubles()
440                 bpy.ops.mesh.quads_convert_to_tris()
441                 bpy.ops.mesh.tris_convert_to_quads()
442                 bpy.ops.object.mode_set(mode='OBJECT')
443             final_objects += split_objects
444
445         ob.select_set(False)
446
447     print(final_objects)
448     return final_objects
449
450
451 def export_map(context, filepath):
452     """
453     pup_block = [\
454     ('Scale:', PREF_SCALE, 1, 1000,
455             'Scale the blender scene by this value.'),\
456     ('Face Width:', PREF_FACE_THICK, 0.01, 10,
457             'Thickness of faces exported as brushes.'),\
458     ('Grid Snap', PREF_GRID_SNAP,
459             'snaps floating point values to whole numbers.'),\
460     'Null Texture',\
461     ('', PREF_NULL_TEX, 1, 128,
462             'Export textureless faces with this texture'),\
463     'Unseen Texture',\
464     ('', PREF_INVIS_TEX, 1, 128,
465             'Export invisible faces with this texture'),\
466     ]
467
468     if not Draw.PupBlock('map export', pup_block):
469         return
470     """
471     import time
472     from mathutils import Matrix
473     from bpy_extras import mesh_utils
474
475     t = time.time()
476     print("Map Exporter 0.0")
477
478     scene = context.scene
479     objects = context.selected_objects
480
481     obs_mesh = []
482     obs_lamp = []
483     obs_surf = []
484     obs_empty = []
485
486     SCALE_MAT = Matrix()
487     SCALE_MAT[0][0] = SCALE_MAT[1][1] = SCALE_MAT[2][2] = PREF_SCALE
488
489     TOTBRUSH = TOTLAMP = TOTNODE = 0
490
491     for ob in objects:
492         type = ob.type
493         if type == 'MESH':
494             obs_mesh.append(ob)
495         elif type == 'SURFACE':
496             obs_surf.append(ob)
497         elif type == 'LAMP':
498             obs_lamp.append(ob)
499         elif type == 'EMPTY':
500             obs_empty.append(ob)
501
502     obs_mesh = split_objects(context, obs_mesh)
503
504     with open(filepath, 'w') as fl:
505         fw = fl.write
506
507         if obs_mesh or obs_surf:
508             if PREF_DOOM3_FORMAT:
509                 fw('Version 2')
510
511             # brushes and surf's must be under worldspan
512             fw('\n// entity 0\n')
513             fw('{\n')
514             fw('"classname" "worldspawn"\n')
515
516         print("\twriting cubes from meshes")
517
518         tot_ob = len(obs_mesh)
519         for i, ob in enumerate(obs_mesh):
520             print("Exporting object: %d/%d" % (i, tot_ob))
521
522             dummy_mesh = ob.to_mesh(scene, True, 'PREVIEW')
523
524             #print len(mesh_split2connected(dummy_mesh))
525
526             # 1 to tx the normals also
527             dummy_mesh.transform(ob.matrix_world * SCALE_MAT)
528
529             # High quality normals
530             #XXX25: BPyMesh.meshCalcNormals(dummy_mesh)
531
532             if PREF_DOOM3_FORMAT:
533                 for me in split_mesh_in_convex_parts(dummy_mesh):
534                     write_doom_brush(fw, ob, me)
535                     TOTBRUSH += 1
536                     if (me is not dummy_mesh):
537                         bpy.data.meshes.remove(me)
538             else:
539                 # We need tessfaces
540                 dummy_mesh.update(calc_tessface=True)
541                 # Split mesh into connected regions
542                 for face_group in mesh_utils.mesh_linked_tessfaces(dummy_mesh):
543                     if is_cube_facegroup(face_group):
544                         write_quake_brush_cube(fw, ob, face_group)
545                         TOTBRUSH += 1
546                     elif is_tricyl_facegroup(face_group):
547                         write_quake_brush_cube(fw, ob, face_group)
548                         TOTBRUSH += 1
549                     else:
550                         for f in face_group:
551                             write_quake_brush_face(fw, ob, f)
552                             TOTBRUSH += 1
553
554                 #print 'warning, not exporting "%s" it is not a cube' % ob.name
555                 bpy.data.meshes.remove(dummy_mesh)
556
557         valid_dims = 3, 5, 7, 9, 11, 13, 15
558         for ob in obs_surf:
559             '''
560             Surf, patches
561             '''
562             data = ob.data
563             surf_name = data.name
564             mat = ob.matrix_world * SCALE_MAT
565
566             # This is what a valid patch looks like
567             """
568             // brush 0
569             {
570             patchDef2
571             {
572             NULL
573             ( 3 3 0 0 0 )
574             (
575             ( ( -64 -64 0 0 0 ) ( -64 0 0 0 -2 ) ( -64 64 0 0 -4 ) )
576             ( ( 0 -64 0 2 0 ) ( 0 0 0 2 -2 ) ( 0 64 0 2 -4 ) )
577             ( ( 64 -64 0 4 0 ) ( 64 0 0 4 -2 ) ( 80 88 0 4 -4 ) )
578             )
579             }
580             }
581             """
582             for i, nurb in enumerate(data.splines):
583                 u = nurb.point_count_u
584                 v = nurb.point_count_v
585                 if u in valid_dims and v in valid_dims:
586
587                     fw('// brush %d surf_name\n' % i)
588                     fw('{\n')
589                     fw('patchDef2\n')
590                     fw('{\n')
591                     fw('NULL\n')
592                     fw('( %d %d 0 0 0 )\n' % (u, v))
593                     fw('(\n')
594
595                     u_iter = 0
596                     for p in nurb.points:
597
598                         if u_iter == 0:
599                             fw('(')
600
601                         u_iter += 1
602
603                         # add nmapping 0 0 ?
604                         if PREF_GRID_SNAP:
605                             fw(" ( %d %d %d 0 0 )" %
606                                        round_vec(mat * p.co.xyz))
607                         else:
608                             fw(' ( %.6f %.6f %.6f 0 0 )' %
609                                        (mat * p.co.xyz)[:])
610
611                         # Move to next line
612                         if u_iter == u:
613                             fw(' )\n')
614                             u_iter = 0
615
616                     fw(')\n')
617                     fw('}\n')
618                     fw('}\n')
619                     # Debugging
620                     # for p in nurb: print 'patch', p
621
622                 else:
623                     print("Warning: not exporting patch",
624                           surf_name, u, v, 'Unsupported')
625
626         if obs_mesh or obs_surf:
627             fw('}\n')  # end worldspan
628
629         print("\twriting lamps")
630         for ob in obs_lamp:
631             print("\t\t%s" % ob.name)
632             lamp = ob.data
633             fw('{\n')
634             fw('"classname" "light"\n')
635             fw('"light" "%.6f"\n' % (lamp.distance * PREF_SCALE))
636             if PREF_GRID_SNAP:
637                 fw('"origin" "%d %d %d"\n' %
638                            tuple([round(axis * PREF_SCALE)
639                                   for axis in ob.matrix_world.to_translation()]))
640             else:
641                 fw('"origin" "%.6f %.6f %.6f"\n' %
642                            tuple([axis * PREF_SCALE
643                                   for axis in ob.matrix_world.to_translation()]))
644
645             fw('"_color" "%.6f %.6f %.6f"\n' % tuple(lamp.color))
646             fw('"style" "0"\n')
647             fw('}\n')
648             TOTLAMP += 1
649
650         print("\twriting empty objects as nodes")
651         for ob in obs_empty:
652             if write_node_map(fw, ob):
653                 print("\t\t%s" % ob.name)
654                 TOTNODE += 1
655             else:
656                 print("\t\tignoring %s" % ob.name)
657
658     for ob in obs_mesh:
659         scene.objects.unlink(ob)
660         bpy.data.objects.remove(ob)
661
662     print("Exported Map in %.4fsec" % (time.time() - t))
663     print("Brushes: %d  Nodes: %d  Lamps %d\n" % (TOTBRUSH, TOTNODE, TOTLAMP))
664
665
666 def save(operator,
667          context,
668          filepath=None,
669          global_scale=1.0,
670          face_thickness=0.1,
671          texture_null="NULL",
672          texture_opts='0 0 0 1 1 0 0 0',
673          grid_snap=False,
674          doom3_format=True,
675          ):
676
677     global PREF_SCALE
678     global PREF_FACE_THICK
679     global PREF_NULL_TEX
680     global PREF_DEF_TEX_OPTS
681     global PREF_GRID_SNAP
682     global PREF_DOOM3_FORMAT
683
684     PREF_SCALE = global_scale
685     PREF_FACE_THICK = face_thickness
686     PREF_NULL_TEX = texture_null
687     PREF_DEF_TEX_OPTS = texture_opts
688     PREF_GRID_SNAP = grid_snap
689     PREF_DOOM3_FORMAT = doom3_format
690
691     if (PREF_DOOM3_FORMAT):
692         PREF_DEF_TEX_OPTS = '0 0 0'
693     else:
694         PREF_DEF_TEX_OPTS = '0 0 0 1 1 0 0 0'
695
696     export_map(context, filepath)
697
698     return {'FINISHED'}