Scripts:
[blender.git] / release / scripts / lightwave_export.py
1 #!BPY
2
3 """
4 Name: 'LightWave (.lwo)...'
5 Blender: 232
6 Group: 'Export'
7 Tooltip: 'Export selected meshes to LightWave File Format (.lwo)'
8 """
9
10 __author__ = "Anthony D'Agostino (Scorpius)"
11 __url__ = ("blender", "elysiun",
12 "Author's homepage, http://www.redrival.com/scorpius")
13 __version__ = "Part of IOSuite 0.5"
14
15 __bpydoc__ = """\
16 This script exports meshes to LightWave file format.
17
18 LightWave is a full-featured commercial modeling and rendering
19 application. The lwo file format is composed of 'chunks,' is well
20 defined, and easy to read and write. It is similar in structure to the
21 trueSpace cob format.
22
23 Usage:<br>
24         Select meshes to be exported and run this script from "File->Export" menu.
25
26 Supported:<br>
27         UV Coordinates, Meshes, Materials, Material Indices, Specular
28 Highlights, and Vertex Colors. For added functionality, each object is
29 placed on its own layer.
30
31 Missing:<br>
32         Not too much, I hope! :).
33
34 Known issues:<br>
35         Empty objects crash has been fixed.
36
37 Notes:<br>
38         For compatibility reasons, it also reads lwo files in the old LW
39 v5.5 format.
40 """
41
42 # $Id$
43 #
44 # +---------------------------------------------------------+
45 # | Copyright (c) 2002 Anthony D'Agostino                   |
46 # | http://www.redrival.com/scorpius                        |
47 # | scorpius@netzero.com                                    |
48 # | April 21, 2002                                          |
49 # | Released under the Blender Artistic Licence (BAL)       |
50 # | Import Export Suite v0.5                                |
51 # +---------------------------------------------------------+
52 # | Read and write LightWave Object File Format (*.lwo)     |
53 # +---------------------------------------------------------+
54
55 import Blender, mod_meshtools
56 import struct, chunk, os, cStringIO, time, operator
57
58 # ==============================
59 # === Write LightWave Format ===
60 # ==============================
61 def write(filename):
62         start = time.clock()
63         file = open(filename, "wb")
64
65         objects = Blender.Object.GetSelected()
66         objects.sort(lambda a,b: cmp(a.name, b.name))
67         if not objects:
68                 mod_meshtools.print_boxed("No mesh objects are selected.")
69                 return
70
71         if len(objects) > 20 and mod_meshtools.show_progress:
72                 mod_meshtools.show_progress = 0
73
74         text = generate_text()
75         desc = generate_desc()
76         icon = "" #generate_icon()
77
78         material_names = get_used_material_names(objects)
79         tags = generate_tags(material_names)
80         surfs = generate_surfs(material_names)
81         chunks = [text, desc, icon, tags]
82
83         meshdata = cStringIO.StringIO()
84         layer_index = 0
85         for object in objects:
86                 objname = object.name
87                 meshname = object.data.name
88                 mesh = Blender.NMesh.GetRaw(meshname)
89                 #mesh = Blender.NMesh.GetRawFromObject(meshname)        # for SubSurf
90                 obj = Blender.Object.Get(objname)
91                 if not mesh: continue
92
93                 layr = generate_layr(objname, layer_index)
94                 pnts = generate_pnts(mesh, obj.matrix)
95                 bbox = generate_bbox(mesh)
96                 pols = generate_pols(mesh)
97                 ptag = generate_ptag(mesh, material_names)
98
99                 if mesh.hasFaceUV():
100                         vmad_uv = generate_vmad_uv(mesh)  # per face
101
102                 if mod_meshtools.has_vertex_colors(mesh):
103                         if mod_meshtools.average_vcols:
104                                 vmap_vc = generate_vmap_vc(mesh)  # per vert
105                         else:
106                                 vmad_vc = generate_vmad_vc(mesh)  # per face
107
108                 write_chunk(meshdata, "LAYR", layr); chunks.append(layr)
109                 write_chunk(meshdata, "PNTS", pnts); chunks.append(pnts)
110                 write_chunk(meshdata, "BBOX", bbox); chunks.append(bbox)
111                 write_chunk(meshdata, "POLS", pols); chunks.append(pols)
112                 write_chunk(meshdata, "PTAG", ptag); chunks.append(ptag)
113
114                 if mesh.hasFaceUV():
115                         write_chunk(meshdata, "VMAD", vmad_uv)
116                         chunks.append(vmad_uv)
117
118                 if mod_meshtools.has_vertex_colors(mesh):
119                         if mod_meshtools.average_vcols:
120                                 write_chunk(meshdata, "VMAP", vmap_vc)
121                                 chunks.append(vmap_vc)
122                         else:
123                                 write_chunk(meshdata, "VMAD", vmad_vc)
124                                 chunks.append(vmad_vc)
125                 layer_index += 1
126
127         for surf in surfs:
128                 chunks.append(surf)
129
130         write_header(file, chunks)
131         write_chunk(file, "ICON", icon)
132         write_chunk(file, "TEXT", text)
133         write_chunk(file, "DESC", desc)
134         write_chunk(file, "TAGS", tags)
135         file.write(meshdata.getvalue()); meshdata.close()
136         for surf in surfs:
137                 write_chunk(file, "SURF", surf)
138
139         Blender.Window.DrawProgressBar(1.0, "")    # clear progressbar
140         file.close()
141         print '\a\r',
142         end = time.clock()
143         seconds = " in %.2f %s" % (end-start, "seconds")
144         message = "Successfully exported " + os.path.basename(filename) + seconds
145         mod_meshtools.print_boxed(message)
146
147 # =======================================
148 # === Generate Null-Terminated String ===
149 # =======================================
150 def generate_nstring(string):
151         if len(string)%2 == 0:  # even
152                 string += "\0\0"
153         else:                                   # odd
154                 string += "\0"
155         return string
156
157 # ===============================
158 # === Get Used Material Names ===
159 # ===============================
160 def get_used_material_names(objects):
161         matnames = {}
162         for object in objects:
163                 objname = object.name
164                 meshname = object.data.name
165                 mesh = Blender.NMesh.GetRaw(meshname)
166                 if not mesh: continue
167                 if (not mesh.materials) and (mod_meshtools.has_vertex_colors(mesh)):
168                         # vcols only
169                         if mod_meshtools.average_vcols:
170                                 matnames["\251 Per-Vert Vertex Colors"] = None
171                         else:
172                                 matnames["\251 Per-Face Vertex Colors"] = None
173                 elif (mesh.materials) and (not mod_meshtools.has_vertex_colors(mesh)):
174                         # materials only
175                         for material in mesh.materials:
176                                 matnames[material.name] = None
177                 elif (not mesh.materials) and (not mod_meshtools.has_vertex_colors(mesh)):
178                         # neither
179                         matnames["\251 Blender Default"] = None
180                 else:
181                         # both
182                         for material in mesh.materials:
183                                 matnames[material.name] = None
184         return matnames
185
186 # =========================================
187 # === Generate Tag Strings (TAGS Chunk) ===
188 # =========================================
189 def generate_tags(material_names):
190         material_names = map(generate_nstring, material_names.keys())
191         tags_data = reduce(operator.add, material_names)
192         return tags_data
193
194 # ========================
195 # === Generate Surface ===
196 # ========================
197 def generate_surface(name, mesh):
198         if name.find("\251 Per-") == 0:
199                 return generate_vcol_surf(mesh)
200         elif name == "\251 Blender Default":
201                 return generate_default_surf()
202         else:
203                 return generate_surf(name)
204
205 # ======================
206 # === Generate Surfs ===
207 # ======================
208 def generate_surfs(material_names):
209         keys = material_names.keys()
210         values = material_names.values()
211         surfaces = map(generate_surface, keys, values)
212         return surfaces
213
214 # ===================================
215 # === Generate Layer (LAYR Chunk) ===
216 # ===================================
217 def generate_layr(name, idx):
218         data = cStringIO.StringIO()
219         data.write(struct.pack(">h", idx))          # layer number
220         data.write(struct.pack(">h", 0))            # flags
221         data.write(struct.pack(">fff", 0, 0, 0))    # pivot
222         data.write(generate_nstring(name))                      # name
223         return data.getvalue()
224
225 # ===================================
226 # === Generate Verts (PNTS Chunk) ===
227 # ===================================
228 def generate_pnts(mesh, matrix):
229         data = cStringIO.StringIO()
230         for i in range(len(mesh.verts)):
231                 if not i%100 and mod_meshtools.show_progress:
232                         Blender.Window.DrawProgressBar(float(i)/len(mesh.verts), "Writing Verts")
233                 x, y, z = mod_meshtools.apply_transform(mesh.verts[i].co, matrix)
234                 data.write(struct.pack(">fff", x, z, y))
235         return data.getvalue()
236
237 # ==========================================
238 # === Generate Bounding Box (BBOX Chunk) ===
239 # ==========================================
240 def generate_bbox(mesh):
241         data = cStringIO.StringIO()
242         # need to transform verts here
243         nv = map(getattr, mesh.verts, ["co"]*len(mesh.verts))
244         xx = map(operator.getitem, nv, [0]*len(nv))
245         yy = map(operator.getitem, nv, [1]*len(nv))
246         zz = map(operator.getitem, nv, [2]*len(nv))
247         data.write(struct.pack(">6f", min(xx), min(zz), min(yy), max(xx), max(zz), max(yy)))
248         return data.getvalue()
249
250 # ========================================
251 # === Average All Vertex Colors (Fast) ===
252 # ========================================
253 def average_vertexcolors(mesh):
254         vertexcolors = {}
255         vcolor_add = lambda u, v: [u[0]+v[0], u[1]+v[1], u[2]+v[2], u[3]+v[3]]
256         vcolor_div = lambda u, s: [u[0]/s, u[1]/s, u[2]/s, u[3]/s]
257         for i in range(len(mesh.faces)):        # get all vcolors that share this vertex
258                 if not i%100 and mod_meshtools.show_progress:
259                         Blender.Window.DrawProgressBar(float(i)/len(mesh.verts), "Finding Shared VColors")
260                 for j in range(len(mesh.faces[i].v)):
261                         index = mesh.faces[i].v[j].index
262                         color = mesh.faces[i].col[j]
263                         r,g,b,a = color.r, color.g, color.b, color.a
264                         vertexcolors.setdefault(index, []).append([r,g,b,a])
265         for i in range(len(vertexcolors)):      # average them
266                 if not i%100 and mod_meshtools.show_progress:
267                         Blender.Window.DrawProgressBar(float(i)/len(mesh.verts), "Averaging Vertex Colors")
268                 vcolor = [0,0,0,0]      # rgba
269                 for j in range(len(vertexcolors[i])):
270                         vcolor = vcolor_add(vcolor, vertexcolors[i][j])
271                 shared = len(vertexcolors[i])
272                 vertexcolors[i] = vcolor_div(vcolor, shared)
273         return vertexcolors
274
275 # ====================================================
276 # === Generate Per-Vert Vertex Colors (VMAP Chunk) ===
277 # ====================================================
278 def generate_vmap_vc(mesh):
279         data = cStringIO.StringIO()
280         data.write("RGB ")                                      # type
281         data.write(struct.pack(">H", 3))                        # dimension
282         data.write(generate_nstring("Blender's Vertex Colors")) # name
283         vertexcolors = average_vertexcolors(mesh)
284         for i in range(len(vertexcolors)):
285                 r, g, b, a = vertexcolors[i]
286                 data.write(struct.pack(">H", i)) # vertex index
287                 data.write(struct.pack(">fff", r/255.0, g/255.0, b/255.0))
288         return data.getvalue()
289
290 # ====================================================
291 # === Generate Per-Face Vertex Colors (VMAD Chunk) ===
292 # ====================================================
293 def generate_vmad_vc(mesh):
294         data = cStringIO.StringIO()
295         data.write("RGB ")                                      # type
296         data.write(struct.pack(">H", 3))                        # dimension
297         data.write(generate_nstring("Blender's Vertex Colors")) # name
298         for i in range(len(mesh.faces)):
299                 if not i%100 and mod_meshtools.show_progress:
300                         Blender.Window.DrawProgressBar(float(i)/len(mesh.faces), "Writing Vertex Colors")
301                 numfaceverts = len(mesh.faces[i].v)
302                 for j in range(numfaceverts-1, -1, -1):                         # Reverse order
303                         r = mesh.faces[i].col[j].r
304                         g = mesh.faces[i].col[j].g
305                         b = mesh.faces[i].col[j].b
306                         v = mesh.faces[i].v[j].index
307                         data.write(struct.pack(">H", v)) # vertex index
308                         data.write(struct.pack(">H", i)) # face index
309                         data.write(struct.pack(">fff", r/255.0, g/255.0, b/255.0))
310         return data.getvalue()
311
312 # ================================================
313 # === Generate Per-Face UV Coords (VMAD Chunk) ===
314 # ================================================
315 def generate_vmad_uv(mesh):
316         data = cStringIO.StringIO()
317         data.write("TXUV")                                       # type
318         data.write(struct.pack(">H", 2))                         # dimension
319         data.write(generate_nstring("Blender's UV Coordinates")) # name
320         for i in range(len(mesh.faces)):
321                 if not i%100 and mod_meshtools.show_progress:
322                         Blender.Window.DrawProgressBar(float(i)/len(mesh.faces), "Writing UV Coordinates")
323                 numfaceverts = len(mesh.faces[i].v)
324                 for j in range(numfaceverts-1, -1, -1):                         # Reverse order
325                         U,V = mesh.faces[i].uv[j]
326                         v = mesh.faces[i].v[j].index
327                         data.write(struct.pack(">H", v)) # vertex index
328                         data.write(struct.pack(">H", i)) # face index
329                         data.write(struct.pack(">ff", U, V))
330         return data.getvalue()
331
332 # ======================================
333 # === Generate Variable-Length Index ===
334 # ======================================
335 def generate_vx(index):
336         if index < 0xFF00:
337                 value = struct.pack(">H", index)                 # 2-byte index
338         else:
339                 value = struct.pack(">L", index | 0xFF000000)    # 4-byte index
340         return value
341
342 # ===================================
343 # === Generate Faces (POLS Chunk) ===
344 # ===================================
345 def generate_pols(mesh):
346         data = cStringIO.StringIO()
347         data.write("FACE")  # polygon type
348         for i in range(len(mesh.faces)):
349                 if not i%100 and mod_meshtools.show_progress:
350                         Blender.Window.DrawProgressBar(float(i)/len(mesh.faces), "Writing Faces")
351                 data.write(struct.pack(">H", len(mesh.faces[i].v))) # numfaceverts
352                 numfaceverts = len(mesh.faces[i].v)
353                 for j in range(numfaceverts-1, -1, -1):                         # Reverse order
354                         index = mesh.faces[i].v[j].index
355                         data.write(generate_vx(index))
356         return data.getvalue()
357
358 # =================================================
359 # === Generate Polygon Tag Mapping (PTAG Chunk) ===
360 # =================================================
361 def generate_ptag(mesh, material_names):
362         data = cStringIO.StringIO()
363         data.write("SURF")  # polygon tag type
364         for i in range(len(mesh.faces)): # numfaces
365                 if not i%100 and mod_meshtools.show_progress:
366                         Blender.Window.DrawProgressBar(float(i)/len(mesh.faces), "Writing Surface Indices")
367                 data.write(generate_vx(i))
368                 if (not mesh.materials) and (mod_meshtools.has_vertex_colors(mesh)):            # vcols only
369                         if mod_meshtools.average_vcols:
370                                 name = "\251 Per-Vert Vertex Colors"
371                         else:
372                                 name = "\251 Per-Face Vertex Colors"
373                 elif (mesh.materials) and (not mod_meshtools.has_vertex_colors(mesh)):          # materials only
374                         idx = mesh.faces[i].mat #erialIndex
375                         name = mesh.materials[idx].name
376                 elif (not mesh.materials) and (not mod_meshtools.has_vertex_colors(mesh)):      # neither
377                         name = "\251 Blender Default"
378                 else:                                                                                                                                           # both
379                         idx = mesh.faces[i].mat
380                         name = mesh.materials[idx].name
381                 names = material_names.keys()
382                 surfidx = names.index(name)
383                 data.write(struct.pack(">H", surfidx)) # surface index
384         return data.getvalue()
385
386 # ===================================================
387 # === Generate VC Surface Definition (SURF Chunk) ===
388 # ===================================================
389 def generate_vcol_surf(mesh):
390         data = cStringIO.StringIO()
391         if mod_meshtools.average_vcols and mod_meshtools.has_vertex_colors(mesh):
392                 surface_name = generate_nstring("\251 Per-Vert Vertex Colors")
393         else:
394                 surface_name = generate_nstring("\251 Per-Face Vertex Colors")
395         data.write(surface_name)
396         data.write("\0\0")
397
398         data.write("COLR")
399         data.write(struct.pack(">H", 14))
400         data.write(struct.pack(">fffH", 1, 1, 1, 0))
401
402         data.write("DIFF")
403         data.write(struct.pack(">H", 6))
404         data.write(struct.pack(">fH", 0.0, 0))
405
406         data.write("LUMI")
407         data.write(struct.pack(">H", 6))
408         data.write(struct.pack(">fH", 1.0, 0))
409
410         data.write("VCOL")
411         data.write(struct.pack(">H", 34))
412         data.write(struct.pack(">fH4s", 1.0, 0, "RGB "))  # intensity, envelope, type
413         data.write(generate_nstring("Blender's Vertex Colors")) # name
414
415         data.write("CMNT")  # material comment
416         comment = "Vertex Colors: Exported from Blender\256 " + mod_meshtools.blender_version_str
417         comment = generate_nstring(comment)
418         data.write(struct.pack(">H", len(comment)))
419         data.write(comment)
420         return data.getvalue()
421
422 # ================================================
423 # === Generate Surface Definition (SURF Chunk) ===
424 # ================================================
425 def generate_surf(material_name):
426         data = cStringIO.StringIO()
427         data.write(generate_nstring(material_name))
428         data.write("\0\0")
429
430         material = Blender.Material.Get(material_name)
431         R,G,B = material.R, material.G, material.B
432         data.write("COLR")
433         data.write(struct.pack(">H", 14))
434         data.write(struct.pack(">fffH", R, G, B, 0))
435
436         data.write("DIFF")
437         data.write(struct.pack(">H", 6))
438         data.write(struct.pack(">fH", material.ref, 0))
439
440         data.write("LUMI")
441         data.write(struct.pack(">H", 6))
442         data.write(struct.pack(">fH", material.emit, 0))
443
444         data.write("SPEC")
445         data.write(struct.pack(">H", 6))
446         data.write(struct.pack(">fH", material.spec, 0))
447
448         data.write("GLOS")
449         data.write(struct.pack(">H", 6))
450         gloss = material.hard / (255/2.0)
451         gloss = round(gloss, 1)
452         data.write(struct.pack(">fH", gloss, 0))
453
454         data.write("CMNT")  # material comment
455         comment = material_name + ": Exported from Blender\256 " + mod_meshtools.blender_version_str
456         comment = generate_nstring(comment)
457         data.write(struct.pack(">H", len(comment)))
458         data.write(comment)
459         return data.getvalue()
460
461 # =============================================
462 # === Generate Default Surface (SURF Chunk) ===
463 # =============================================
464 def generate_default_surf():
465         data = cStringIO.StringIO()
466         material_name = "\251 Blender Default"
467         data.write(generate_nstring(material_name))
468         data.write("\0\0")
469
470         data.write("COLR")
471         data.write(struct.pack(">H", 14))
472         data.write(struct.pack(">fffH", 1, 1, 1, 0))
473
474         data.write("DIFF")
475         data.write(struct.pack(">H", 6))
476         data.write(struct.pack(">fH", 0.8, 0))
477
478         data.write("LUMI")
479         data.write(struct.pack(">H", 6))
480         data.write(struct.pack(">fH", 0, 0))
481
482         data.write("SPEC")
483         data.write(struct.pack(">H", 6))
484         data.write(struct.pack(">fH", 0.5, 0))
485
486         data.write("GLOS")
487         data.write(struct.pack(">H", 6))
488         gloss = 50 / (255/2.0)
489         gloss = round(gloss, 1)
490         data.write(struct.pack(">fH", gloss, 0))
491
492         data.write("CMNT")  # material comment
493         comment = material_name + ": Exported from Blender\256 " + mod_meshtools.blender_version_str
494
495         # vals = map(chr, range(164,255,1))
496         # keys = range(164,255,1)
497         # keys = map(lambda x: `x`, keys)
498         # comment = map(None, keys, vals)
499         # comment = reduce(operator.add, comment)
500         # comment = reduce(operator.add, comment)
501
502         comment = generate_nstring(comment)
503         data.write(struct.pack(">H", len(comment)))
504         data.write(comment)
505         return data.getvalue()
506
507 # ============================================
508 # === Generate Object Comment (TEXT Chunk) ===
509 # ============================================
510 def generate_text():
511         comment  = "Lightwave Export Script for Blender "
512         comment +=      mod_meshtools.blender_version_str + "\n"
513         comment += "by Anthony D'Agostino\n"
514         comment += "scorpius@netzero.com\n"
515         comment += "http://ourworld.compuserve.com/homepages/scorpius\n"
516         return generate_nstring(comment)
517
518 # ==============================================
519 # === Generate Description Line (DESC Chunk) ===
520 # ==============================================
521 def generate_desc():
522         comment = "Copyright 2002 Scorpius Entertainment"
523         return generate_nstring(comment)
524
525 # ==================================================
526 # === Generate Thumbnail Icon Image (ICON Chunk) ===
527 # ==================================================
528 def generate_icon():
529         data = cStringIO.StringIO()
530         file = open("f:/obj/radiosity/lwo2_icon.tga", "rb") # 60x60 uncompressed TGA
531         file.read(18)
532         icon_data = file.read(3600) # ?
533         file.close()
534         data.write(struct.pack(">HH", 0, 60))
535         data.write(icon_data)
536         #print len(icon_data)
537         return data.getvalue()
538
539 # ===================
540 # === Write Chunk ===
541 # ===================
542 def write_chunk(file, name, data):
543         file.write(name)
544         file.write(struct.pack(">L", len(data)))
545         file.write(data)
546
547 # =============================
548 # === Write LWO File Header ===
549 # =============================
550 def write_header(file, chunks):
551         chunk_sizes = map(len, chunks)
552         chunk_sizes = reduce(operator.add, chunk_sizes)
553         form_size = chunk_sizes + len(chunks)*8 + len("FORM")
554         file.write("FORM")
555         file.write(struct.pack(">L", form_size))
556         file.write("LWO2")
557
558 def fs_callback(filename):
559         if filename.find('.lwo', -4) <= 0: filename += '.lwo'
560         write(filename)
561
562 Blender.Window.FileSelector(fs_callback, "Export LWO")