io_export_ogreDotScene.py
Go to the documentation of this file.
1 # Copyright (C) 2010 Brett Hartshorn
2 #
3 # This library is free software; you can redistribute it and/or
4 # modify it under the terms of the GNU Lesser General Public
5 # License as published by the Free Software Foundation; either
6 # version 2.1 of the License, or (at your option) any later version.
7 #
8 # This library 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 GNU
11 # Lesser General Public License for more details.
12 #
13 # You should have received a copy of the GNU Lesser General Public
14 # License along with this library; if not, write to the Free Software
15 # Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
16 
17 VERSION = '0.6.0'
18 
19 '''
20 CHANGELOG
21  0.6.0
22  * patched to work with 2.66.
23  0.5.9
24  * apply patch from Thomas for Blender 2.6x support
25  0.5.8
26  * Clean all names that will be used as filenames on disk. Adjust all places
27  that use these names for refs instead of ob.name/ob.data.name. Replaced chars
28  are \, /, :, *, ?, ", <, >, | and spaces. Tested on work with ogre
29  material, mesh and skeleton writing/refs inside the files and txml refs.
30  Shows warning at final report if we had to resort to the renaming so user
31  can possibly rename the object.
32  * Added silent auto update checks if blender2ogre was installed using
33  the .exe installer. This will keep people up to date when new versions are out.
34  * Fix tracker issue 48: Needs to check if outputting to /tmp or
35  ~/.wine/drive_c/tmp on Linux. Thanks to vax456 for providing the patch,
36  added him to contributors. Preview mesh's are now placed under /tmp
37  on Linux systems if the OgreMeshy executable ends with .exe
38  * Fix tracker issue 46: add operationtype to <submesh>
39  * Implement a modal dialog that reports if material names have invalid
40  characters and cant be saved on disk. This small popup will show until
41  user presses left or right mouse (anywhere).
42  * Fix tracker issue 44: XML Attributes not properly escaped in .scene file
43  * Implemented reading OgreXmlConverter path from windows registry.
44  The .exe installer will ship with certain tools so we can stop guessing
45  and making the user install tools separately and setting up paths.
46  * Fix bug that .mesh files were not generated while doing a .txml export.
47  This was result of the late 2.63 mods that forgot to update object
48  facecount before determining if mesh should be exported.
49  * Fix bug that changed settings in the export dialog were forgotten when you
50  re-exported without closing blender. Now settings should persist always
51  from the last export. They are also stored to disk so the same settings
52  are in use when if you restart Blender.
53  * Fix bug that once you did a export, the next time the export location was
54  forgotten. Now on sequential exports, the last export path is remembered in
55  the export dialog.
56  * Remove all local:// from asset refs and make them relative in .txml export.
57  Having relative refs is the best for local preview and importing the txml
58  to existing scenes.
59  * Make .material generate what version of this plugins was used to generate
60  the material file. Will be helpful in production to catch things.
61  Added pretty printing line endings so the raw .material data is easier to read.
62  * Improve console logging for the export stages. Run Blender from
63  cmd prompt to see this information.
64  * Clean/fix documentation in code for future development
65  * Add todo to code for future development
66  * Restructure/move code for easier readability
67  * Remove extra white spaces and convert tabs to space
68  0.5.7
69  * Update to Blender 2.6.3.
70  * Fixed xz-y Skeleton rotation (again)
71  * Added additional Keyframe at the end of each animation to prevent
72  ogre from interpolating back to the start
73  * Added option to ignore non-deformable bones
74  * Added option to export nla-strips independently from each other
75 
76 TODO
77  * Remove this section and integrate below with code :)
78  * Fix terrain collision offset bug
79  * Add realtime transform (rotation is missing)
80  * Fix camera rotated -90 ogre-dot-scene
81  * Set description field for all pyRNA
82 '''
83 
84 bl_info = {
85  "name": "OGRE Exporter (.scene, .mesh, .skeleton) and RealXtend (.txml)",
86  "author": "Brett, S.Rombauts, F00bar, Waruck, Mind Calamity, Mr.Magne, Jonne Nauha, vax456",
87  "version": (0, 6, 0),
88  "blender": (2, 6, 6),
89  "location": "File > Export...",
90  "description": "Export to Ogre xml and binary formats",
91  "wiki_url": "http://code.google.com/p/blender2ogre/w/list",
92  "tracker_url": "http://code.google.com/p/blender2ogre/issues/list",
93  "category": "Import-Export"
94 }
95 
96 ## Public API
97 ## Search for "Public API" to find more
98 
99 UI_CLASSES = []
100 
101 def UI(cls):
102  ''' Toggles the Ogre interface panels '''
103  if cls not in UI_CLASSES:
104  UI_CLASSES.append(cls)
105  return cls
106 
108  for cls in UI_CLASSES:
109  bpy.utils.unregister_class( cls )
110 
111 def uid(ob):
112  if ob.uid == 0:
113  high = 0
114  multires = 0
115  for o in bpy.data.objects:
116  if o.uid > high: high = o.uid
117  if o.use_multires_lod: multires += 1
118  high += 1 + (multires*10)
119  if high < 100: high = 100 # start at 100
120  ob.uid = high
121  return ob.uid
122 
123 ## Imports
124 
125 import os, sys, time, array, ctypes, math
126 
127 try:
128  # Inside blender
129  import bpy, mathutils
130  from bpy.props import *
131 except ImportError:
132  # If we end up here we are outside blender (compile optional C module)
133  assert __name__ == '__main__'
134  print('Trying to compile Rpython C-library')
135  assert sys.version_info.major == 2 # rpython only works from Python2
136  print('...searching for rpythonic...')
137  sys.path.append('../rpythonic')
138  import rpythonic
139  rpythonic.set_pypy_root( '../pypy' )
140  import pypy.rpython.lltypesystem.rffi as rffi
141  from pypy.rlib import streamio
142  rpy = rpythonic.RPython( 'blender2ogre' )
143 
144  @rpy.bind(
145  path=str,
146  facesAddr=int,
147  facesSmoothAddr=int,
148  facesMatAddr=int,
149  vertsPosAddr=int,
150  vertsNorAddr=int,
151  numFaces=int,
152  numVerts=int,
153  materialNames=str, # [str] is too tricky to convert py-list to rpy-list
154  )
155 
156  def dotmesh( path, facesAddr, facesSmoothAddr, facesMatAddr, vertsPosAddr, vertsNorAddr, numFaces, numVerts, materialNames ):
157  print('PATH----------------', path)
158  materials = []
159  for matname in materialNames.split(';'):
160  print( 'Material Name: %s' %matname )
161  materials.append( matname )
162 
163  file = streamio.open_file_as_stream( path, 'w')
164 
165  faces = rffi.cast( rffi.UINTP, facesAddr ) # face vertex indices
166  facesSmooth = rffi.cast( rffi.CCHARP, facesSmoothAddr )
167  facesMat = rffi.cast( rffi.USHORTP, facesMatAddr )
168 
169  vertsPos = rffi.cast( rffi.FLOATP, vertsPosAddr )
170  vertsNor = rffi.cast( rffi.FLOATP, vertsNorAddr )
171 
172  VB = [
173  '<sharedgeometry>',
174  '<vertexbuffer positions="true" normals="true">'
175  ]
176  fastlookup = {}
177  ogre_vert_index = 0
178  triangles = []
179  for fidx in range( numFaces ):
180  smooth = ord( facesSmooth[ fidx ] ) # ctypes.c_bool > char > int
181 
182  matidx = facesMat[ fidx ]
183  i = fidx*4
184  ai = faces[ i ]; bi = faces[ i+1 ]
185  ci = faces[ i+2 ]; di = faces[ i+3 ]
186 
187  triangle = []
188  for J in [ai, bi, ci]:
189  i = J*3
190  x = rffi.cast( rffi.DOUBLE, vertsPos[ i ] )
191  y = rffi.cast( rffi.DOUBLE, vertsPos[ i+1 ] )
192  z = rffi.cast( rffi.DOUBLE, vertsPos[ i+2 ] )
193  pos = (x,y,z)
194  #if smooth:
195  x = rffi.cast( rffi.DOUBLE, vertsNor[ i ] )
196  y = rffi.cast( rffi.DOUBLE, vertsNor[ i+1 ] )
197  z = rffi.cast( rffi.DOUBLE, vertsNor[ i+2 ] )
198  nor = (x,y,z)
199 
200  SIG = (pos,nor)#, matidx)
201  skip = False
202  if J in fastlookup:
203  for otherSIG in fastlookup[ J ]:
204  if SIG == otherSIG:
205  triangle.append( fastlookup[J][otherSIG] )
206  skip = True
207  break
208 
209  if not skip:
210  triangle.append( ogre_vert_index )
211  fastlookup[ J ][ SIG ] = ogre_vert_index
212 
213  else:
214  triangle.append( ogre_vert_index )
215  fastlookup[ J ] = { SIG : ogre_vert_index }
216 
217  if skip: continue
218 
219  xml = [
220  '<vertex>',
221  '<position x="%s" y="%s" z="%s" />' %pos, # funny that tuple is valid here
222  '<normal x="%s" y="%s" z="%s" />' %nor,
223  '</vertex>'
224  ]
225  VB.append( '\n'.join(xml) )
226 
227  ogre_vert_index += 1
228 
229  triangles.append( triangle )
230  VB.append( '</vertexbuffer>' )
231  VB.append( '</sharedgeometry>' )
232 
233  file.write( '\n'.join(VB) )
234  del VB # free memory
235 
236  SMS = ['<submeshes>']
237  #for matidx, matname in ...:
238  SM = [
239  '<submesh usesharedvertices="true" use32bitindexes="true" material="%s" operationtype="triangle_list">' % 'somemat',
240  '<faces count="%s">' %'100',
241  ]
242  for tri in triangles:
243  #x,y,z = tri # rpython bug, when in a new 'block' need to unpack/repack tuple
244  #s = '<face v1="%s" v2="%s" v3="%s" />' %(x,y,z)
245  assert isinstance(tri,tuple) #and len(tri)==3 # this also works
246  s = '<face v1="%s" v2="%s" v3="%s" />' %tri # but tuple is not valid here
247  SM.append( s )
248  SM.append( '</faces>' )
249  SM.append( '</submesh>' )
250 
251  file.write( '\n'.join(SM) )
252  file.close()
253 
254  rpy.cache(refresh=1)
255  sys.exit('OK: module compiled and cached')
256 
257 ## More imports now that Blender is imported
258 
259 import hashlib, getpass, tempfile, configparser, subprocess, pickle
260 from xml.sax.saxutils import XMLGenerator, quoteattr
261 
262 class CMesh(object):
263 
264  def __init__(self, data):
265  self.numVerts = N = len( data.vertices )
266  self.numFaces = Nfaces = len(data.tessfaces)
267 
268  self.vertex_positions = (ctypes.c_float * (N * 3))()
269  data.vertices.foreach_get( 'co', self.vertex_positions )
270  v = self.vertex_positions
271 
272  self.vertex_normals = (ctypes.c_float * (N * 3))()
273  data.vertices.foreach_get( 'normal', self.vertex_normals )
274 
275  self.faces = (ctypes.c_uint * (Nfaces * 4))()
276  data.tessfaces.foreach_get( 'vertices_raw', self.faces )
277 
278  self.faces_normals = (ctypes.c_float * (Nfaces * 3))()
279  data.tessfaces.foreach_get( 'normal', self.faces_normals )
280 
281  self.faces_smooth = (ctypes.c_bool * Nfaces)()
282  data.tessfaces.foreach_get( 'use_smooth', self.faces_smooth )
283 
284  self.faces_material_index = (ctypes.c_ushort * Nfaces)()
285  data.tessfaces.foreach_get( 'material_index', self.faces_material_index )
286 
287  self.vertex_colors = []
288  if len( data.vertex_colors ):
289  vc = data.vertex_colors[0]
290  n = len(vc.data)
291  # no colors_raw !!?
292  self.vcolors1 = (ctypes.c_float * (n * 3))() # face1
293  vc.data.foreach_get( 'color1', self.vcolors1 )
294  self.vertex_colors.append( self.vcolors1 )
295 
296  self.vcolors2 = (ctypes.c_float * (n * 3))() # face2
297  vc.data.foreach_get( 'color2', self.vcolors2 )
298  self.vertex_colors.append( self.vcolors2 )
299 
300  self.vcolors3 = (ctypes.c_float * (n * 3))() # face3
301  vc.data.foreach_get( 'color3', self.vcolors3 )
302  self.vertex_colors.append( self.vcolors3 )
303 
304  self.vcolors4 = (ctypes.c_float * (n * 3))() # face4
305  vc.data.foreach_get( 'color4', self.vcolors4 )
306  self.vertex_colors.append( self.vcolors4 )
307 
308  self.uv_textures = []
309  if data.uv_textures.active:
310  for layer in data.uv_textures:
311  n = len(layer.data) * 8
312  a = (ctypes.c_float * n)()
313  layer.data.foreach_get( 'uv_raw', a ) # 4 faces
314  self.uv_textures.append( a )
315 
316  def save( blenderobject, path ):
317  cmesh = Mesh( blenderobject.data )
318  start = time.time()
319  dotmesh(
320  path,
321  ctypes.addressof( cmesh.faces ),
322  ctypes.addressof( cmesh.faces_smooth ),
323  ctypes.addressof( cmesh.faces_material_index ),
324  ctypes.addressof( cmesh.vertex_positions ),
325  ctypes.addressof( cmesh.vertex_normals ),
326  cmesh.numFaces,
327  cmesh.numVerts,
328  )
329  print('Mesh dumped in %s seconds' % (time.time()-start))
330 
331 ## Make sure we can import from same directory
332 SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__))
333 if SCRIPT_DIR not in sys.path:
334  sys.path.append( SCRIPT_DIR )
335 
336 ## Avatar
337 
338 bpy.types.Object.use_avatar = BoolProperty(
339  name='enable avatar',
340  description='enables EC_Avatar',
341  default=False)
342 bpy.types.Object.avatar_reference = StringProperty(
343  name='avatar reference',
344  description='sets avatar reference URL',
345  maxlen=128,
346  default='')
347 BoolProperty( name='enable avatar', description='enables EC_Avatar', default=False) # todo: is this used?
348 
349 # Tundra IDs
350 
351 bpy.types.Object.uid = IntProperty(
352  name="unique ID",
353  description="unique ID for Tundra",
354  default=0, min=0, max=2**14)
355 
356 # Rendering
357 
358 bpy.types.Object.use_draw_distance = BoolProperty(
359  name='enable draw distance',
360  description='use LOD draw distance',
361  default=False)
362 bpy.types.Object.draw_distance = FloatProperty(
363  name='draw distance',
364  description='distance at which to begin drawing object',
365  default=0.0, min=0.0, max=10000.0)
366 bpy.types.Object.cast_shadows = BoolProperty(
367  name='cast shadows',
368  description='cast shadows',
369  default=False)
370 bpy.types.Object.use_multires_lod = BoolProperty(
371  name='Enable Multires LOD',
372  description='enables multires LOD',
373  default=False)
374 bpy.types.Object.multires_lod_range = FloatProperty(
375  name='multires LOD range',
376  description='far distance at which multires is set to base level',
377  default=30.0, min=0.0, max=10000.0)
378 
379 ## Physics
380 
381 _physics_modes = [
382  ('NONE', 'NONE', 'no physics'),
383  ('RIGID_BODY', 'RIGID_BODY', 'rigid body'),
384  ('SOFT_BODY', 'SOFT_BODY', 'soft body'),
385 ]
386 _collision_modes = [
387  ('NONE', 'NONE', 'no collision'),
388  ('PRIMITIVE', 'PRIMITIVE', 'primitive collision type'),
389  ('MESH', 'MESH', 'triangle-mesh or convex-hull collision type'),
390  ('DECIMATED', 'DECIMATED', 'auto-decimated collision type'),
391  ('COMPOUND', 'COMPOUND', 'children primitive compound collision type'),
392  ('TERRAIN', 'TERRAIN', 'terrain (height map) collision type'),
393 ]
394 
395 bpy.types.Object.physics_mode = EnumProperty(
396  items = _physics_modes,
397  name = 'physics mode',
398  description='physics mode',
399  default='NONE')
400 bpy.types.Object.physics_friction = FloatProperty(
401  name='Simple Friction',
402  description='physics friction',
403  default=0.1, min=0.0, max=1.0)
404 bpy.types.Object.physics_bounce = FloatProperty(
405  name='Simple Bounce',
406  description='physics bounce',
407  default=0.01, min=0.0, max=1.0)
408 bpy.types.Object.collision_terrain_x_steps = IntProperty(
409  name="Ogre Terrain: x samples",
410  description="resolution in X of height map",
411  default=64, min=4, max=8192)
412 bpy.types.Object.collision_terrain_y_steps = IntProperty(
413  name="Ogre Terrain: y samples",
414  description="resolution in Y of height map",
415  default=64, min=4, max=8192)
416 bpy.types.Object.collision_mode = EnumProperty(
417  items = _collision_modes,
418  name = 'primary collision mode',
419  description='collision mode',
420  default='NONE')
421 bpy.types.Object.subcollision = BoolProperty(
422  name="collision compound",
423  description="member of a collision compound",
424  default=False)
425 
426 ## Sound
427 
428 bpy.types.Speaker.play_on_load = BoolProperty(
429  name='play on load',
430  default=False)
431 bpy.types.Speaker.loop = BoolProperty(
432  name='loop sound',
433  default=False)
434 bpy.types.Speaker.use_spatial = BoolProperty(
435  name='3D spatial sound',
436  default=True)
437 
438 ## ImageMagick
439 
440 _IMAGE_FORMATS = [
441  ('NONE','NONE', 'do not convert image'),
442  ('bmp', 'bmp', 'bitmap format'),
443  ('jpg', 'jpg', 'jpeg format'),
444  ('gif', 'gif', 'gif format'),
445  ('png', 'png', 'png format'),
446  ('tga', 'tga', 'targa format'),
447  ('dds', 'dds', 'nvidia dds format'),
448 ]
449 
450 bpy.types.Image.use_convert_format = BoolProperty(
451  name='use convert format',
452  default=False
453 )
454 bpy.types.Image.convert_format = EnumProperty(
455  name='convert to format',
456  description='converts to image format using imagemagick',
457  items=_IMAGE_FORMATS,
458  default='NONE')
459 bpy.types.Image.jpeg_quality = IntProperty(
460  name="jpeg quality",
461  description="quality of jpeg",
462  default=80, min=0, max=100)
463 bpy.types.Image.use_color_quantize = BoolProperty(
464  name='use color quantize',
465  default=False)
466 bpy.types.Image.use_color_quantize_dither = BoolProperty(
467  name='use color quantize dither',
468  default=True)
469 bpy.types.Image.color_quantize = IntProperty(
470  name="color quantize",
471  description="reduce to N colors (requires ImageMagick)",
472  default=32, min=2, max=256)
473 bpy.types.Image.use_resize_half = BoolProperty(
474  name='resize by 1/2',
475  default=False)
476 bpy.types.Image.use_resize_absolute = BoolProperty(
477  name='force image resize',
478  default=False)
479 bpy.types.Image.resize_x = IntProperty(
480  name='resize X',
481  description='only if image is larger than defined, use ImageMagick to resize it down',
482  default=256, min=2, max=4096)
483 bpy.types.Image.resize_y = IntProperty(
484  name='resize Y',
485  description='only if image is larger than defined, use ImageMagick to resize it down',
486  default=256, min=2, max=4096)
487 
488 # Materials
489 
490 bpy.types.Material.ogre_depth_write = BoolProperty(
491  # Material.ogre_depth_write = AUTO|ON|OFF
492  name='depth write',
493  default=True)
494 bpy.types.Material.ogre_depth_check = BoolProperty(
495  # If depth-buffer checking is on, whenever a pixel is about to be written to
496  # the frame buffer the depth buffer is checked to see if the pixel is in front
497  # of all other pixels written at that point. If not, the pixel is not written.
498  # If depth checking is off, pixels are written no matter what has been rendered before.
499  name='depth check',
500  default=True)
501 bpy.types.Material.ogre_alpha_to_coverage = BoolProperty(
502  # Sets whether this pass will use 'alpha to coverage', a way to multisample alpha
503  # texture edges so they blend more seamlessly with the background. This facility
504  # is typically only available on cards from around 2006 onwards, but it is safe to
505  # enable it anyway - Ogre will just ignore it if the hardware does not support it.
506  # The common use for alpha to coverage is foliage rendering and chain-link fence style textures.
507  name='multisample alpha edges',
508  default=False)
509 bpy.types.Material.ogre_light_scissor = BoolProperty(
510  # This option is usually only useful if this pass is an additive lighting pass, and is
511  # at least the second one in the technique. Ie areas which are not affected by the current
512  # light(s) will never need to be rendered. If there is more than one light being passed to
513  # the pass, then the scissor is defined to be the rectangle which covers all lights in screen-space.
514  # Directional lights are ignored since they are infinite. This option does not need to be specified
515  # if you are using a standard additive shadow mode, i.e. SHADOWTYPE_STENCIL_ADDITIVE or
516  # SHADOWTYPE_TEXTURE_ADDITIVE, since it is the default behaviour to use a scissor for each additive
517  # shadow pass. However, if you're not using shadows, or you're using Integrated Texture Shadows
518  # where passes are specified in a custom manner, then this could be of use to you.
519  name='light scissor',
520  default=False)
521 bpy.types.Material.ogre_light_clip_planes = BoolProperty(
522  name='light clip planes',
523  default=False)
524 bpy.types.Material.ogre_normalise_normals = BoolProperty(
525  name='normalise normals',
526  default=False,
527  description="Scaling objects causes normals to also change magnitude, which can throw off your lighting calculations. By default, the SceneManager detects this and will automatically re-normalise normals for any scaled object, but this has a cost. If you'd prefer to control this manually, call SceneManager::setNormaliseNormalsOnScale(false) and then use this option on materials which are sensitive to normals being resized.")
528 bpy.types.Material.ogre_lighting = BoolProperty(
529  # Sets whether or not dynamic lighting is turned on for this pass or not. If lighting is turned off,
530  # all objects rendered using the pass will be fully lit. This attribute has no effect if a vertex program is used.
531  name='dynamic lighting',
532  default=True)
533 bpy.types.Material.ogre_colour_write = BoolProperty(
534  # If colour writing is off no visible pixels are written to the screen during this pass. You might think
535  # this is useless, but if you render with colour writing off, and with very minimal other settings,
536  # you can use this pass to initialise the depth buffer before subsequently rendering other passes which
537  # fill in the colour data. This can give you significant performance boosts on some newer cards, especially
538  # when using complex fragment programs, because if the depth check fails then the fragment program is never run.
539  name='color-write',
540  default=True)
541 bpy.types.Material.use_fixed_pipeline = BoolProperty(
542  # Fixed pipeline is oldschool
543  # todo: whats the meaning of this?
544  name='fixed pipeline',
545  default=True)
546 bpy.types.Material.use_material_passes = BoolProperty(
547  # hidden option - gets turned on by operator
548  # todo: What is a hidden option, is this needed?
549  name='use ogre extra material passes (layers)',
550  default=False)
551 bpy.types.Material.use_in_ogre_material_pass = BoolProperty(
552  name='Layer Toggle',
553  default=True)
554 bpy.types.Material.use_ogre_advanced_options = BoolProperty(
555  name='Show Advanced Options',
556  default=False)
557 bpy.types.Material.use_ogre_parent_material = BoolProperty(
558  name='Use Script Inheritance',
559  default=False)
560 bpy.types.Material.ogre_parent_material = EnumProperty(
561  name="Script Inheritence",
562  description='ogre parent material class', #default='NONE',
563  items=[])
564 bpy.types.Material.ogre_polygon_mode = EnumProperty(
565  name='faces draw type',
566  description="ogre face draw mode",
567  items=[ ('solid', 'solid', 'SOLID'),
568  ('wireframe', 'wireframe', 'WIREFRAME'),
569  ('points', 'points', 'POINTS') ],
570  default='solid')
571 bpy.types.Material.ogre_shading = EnumProperty(
572  name='hardware shading',
573  description="Sets the kind of shading which should be used for representing dynamic lighting for this pass.",
574  items=[ ('flat', 'flat', 'FLAT'),
575  ('gouraud', 'gouraud', 'GOURAUD'),
576  ('phong', 'phong', 'PHONG') ],
577  default='gouraud')
578 bpy.types.Material.ogre_cull_hardware = EnumProperty(
579  name='hardware culling',
580  description="If the option 'cull_hardware clockwise' is set, all triangles whose vertices are viewed in clockwise order from the camera will be culled by the hardware.",
581  items=[ ('clockwise', 'clockwise', 'CLOCKWISE'),
582  ('anticlockwise', 'anticlockwise', 'COUNTER CLOCKWISE'),
583  ('none', 'none', 'NONE') ],
584  default='clockwise')
585 bpy.types.Material.ogre_transparent_sorting = EnumProperty(
586  name='transparent sorting',
587  description="By default all transparent materials are sorted such that renderables furthest away from the camera are rendered first. This is usually the desired behaviour but in certain cases this depth sorting may be unnecessary and undesirable. If for example it is necessary to ensure the rendering order does not change from one frame to the next. In this case you could set the value to 'off' to prevent sorting.",
588  items=[ ('on', 'on', 'ON'),
589  ('off', 'off', 'OFF'),
590  ('force', 'force', 'FORCE ON') ],
591  default='on')
592 bpy.types.Material.ogre_illumination_stage = EnumProperty(
593  name='illumination stage',
594  description='When using an additive lighting mode (SHADOWTYPE_STENCIL_ADDITIVE or SHADOWTYPE_TEXTURE_ADDITIVE), the scene is rendered in 3 discrete stages, ambient (or pre-lighting), per-light (once per light, with shadowing) and decal (or post-lighting). Usually OGRE figures out how to categorise your passes automatically, but there are some effects you cannot achieve without manually controlling the illumination.',
595  items=[ ('', '', 'autodetect'),
596  ('ambient', 'ambient', 'ambient'),
597  ('per_light', 'per_light', 'lights'),
598  ('decal', 'decal', 'decal') ],
599  default=''
600 )
601 
602 _ogre_depth_func = [
603  ('less_equal', 'less_equal', '<='),
604  ('less', 'less', '<'),
605  ('equal', 'equal', '=='),
606  ('not_equal', 'not_equal', '!='),
607  ('greater_equal', 'greater_equal', '>='),
608  ('greater', 'greater', '>'),
609  ('always_fail', 'always_fail', 'false'),
610  ('always_pass', 'always_pass', 'true'),
611 ]
612 
613 bpy.types.Material.ogre_depth_func = EnumProperty(
614  items=_ogre_depth_func,
615  name='depth buffer function',
616  description='If depth checking is enabled (see depth_check) a comparison occurs between the depth value of the pixel to be written and the current contents of the buffer. This comparison is normally less_equal, i.e. the pixel is written if it is closer (or at the same distance) than the current contents',
617  default='less_equal')
618 
619 _ogre_scene_blend_ops = [
620  ('add', 'add', 'DEFAULT'),
621  ('subtract', 'subtract', 'SUBTRACT'),
622  ('reverse_subtract', 'reverse_subtract', 'REVERSE SUBTRACT'),
623  ('min', 'min', 'MIN'),
624  ('max', 'max', 'MAX'),
625 ]
626 
627 bpy.types.Material.ogre_scene_blend_op = EnumProperty(
628  items=_ogre_scene_blend_ops,
629  name='scene blending operation',
630  description='This directive changes the operation which is applied between the two components of the scene blending equation',
631  default='add')
632 
633 _ogre_scene_blend_types = [
634  ('one zero', 'one zero', 'DEFAULT'),
635  ('alpha_blend', 'alpha_blend', "The alpha value of the rendering output is used as a mask. Equivalent to 'scene_blend src_alpha one_minus_src_alpha'"),
636  ('add', 'add', "The colour of the rendering output is added to the scene. Good for explosions, flares, lights, ghosts etc. Equivalent to 'scene_blend one one'."),
637  ('modulate', 'modulate', "The colour of the rendering output is multiplied with the scene contents. Generally colours and darkens the scene, good for smoked glass, semi-transparent objects etc. Equivalent to 'scene_blend dest_colour zero'"),
638  ('colour_blend', 'colour_blend', 'Colour the scene based on the brightness of the input colours, but dont darken. Equivalent to "scene_blend src_colour one_minus_src_colour"'),
639 ]
640 for mode in 'dest_colour src_colour one_minus_dest_colour dest_alpha src_alpha one_minus_dest_alpha one_minus_src_alpha'.split():
641  _ogre_scene_blend_types.append( ('one %s'%mode, 'one %s'%mode, '') )
642 del mode
643 
644 bpy.types.Material.ogre_scene_blend = EnumProperty(
645  items=_ogre_scene_blend_types,
646  name='scene blend',
647  description='blending operation of material to scene',
648  default='one zero')
649 
650 ## FAQ
651 
652 _faq_ = '''
653 
654 Q: I have hundres of objects, is there a way i can merge them on export only?
655 A: Yes, just add them to a group named starting with "merge", or link the group.
656 
657 Q: Can i use subsurf or multi-res on a mesh with an armature?
658 A: Yes.
659 
660 Q: Can i use subsurf or multi-res on a mesh with shape animation?
661 A: No.
662 
663 Q: I don't see any objects when i export?
664 A: You must select the objects you wish to export.
665 
666 Q: I don't see my animations when exported?
667 A: Make sure you created an NLA strip on the armature.
668 
669 Q: Do i need to bake my IK and other constraints into FK on my armature before export?
670 A: No.
671 
672 '''
673 
674 ## DOCUMENTATION
675 ''' todo: Update the nonsense C:\Tundra2 paths from defaul config and fix this doc.
676  Additionally point to some doc how to build opengl only version on windows if that really is needed and
677  remove the old Tundra 7z link. '''
678 
679 _doc_installing_ = '''
680 Installing:
681  Installing the Addon:
682  You can simply copy io_export_ogreDotScene.py to your blender installation under blender/2.6x/scripts/addons/
683  and enable it in the user-prefs interface (CTRL+ALT+U)
684  Or you can use blenders interface, under user-prefs, click addons, and click 'install-addon'
685  (its a good idea to delete the old version first)
686 
687  Required:
688  1. Blender 2.63
689 
690  2. Install Ogre Command Line tools to the default path: C:\\OgreCommandLineTools from http://www.ogre3d.org/download/tools
691  * These tools are used to create the binary Mesh from the .xml mesh generated by this plugin.
692  * Linux users may use above and Wine, or install from source, or install via apt-get install ogre-tools.
693 
694  Optional:
695  3. Install NVIDIA DDS Legacy Utilities - Install them to default path.
696  * http://developer.nvidia.com/object/dds_utilities_legacy.html
697  * Linux users will need to use Wine.
698 
699  4. Install Image Magick
700  * http://www.imagemagick.org
701 
702  5. Copy OgreMeshy to C:\\OgreMeshy
703  * If your using 64bit Windows, you may need to download a 64bit OgreMeshy
704  * Linux copy to your home folder.
705 
706  6. realXtend Tundra
707  * For latest Tundra releases see http://code.google.com/p/realxtend-naali/downloads/list
708  - You may need to tweak the config to tell your Tundra path or install to C:\Tundra2
709  * Old OpenGL only build can be found from http://blender2ogre.googlecode.com/files/realxtend-Tundra-2.1.2-OpenGL.7z
710  - Windows: extract to C:\Tundra2
711  - Linux: extract to ~/Tundra2
712 '''
713 
714 ## Options
715 
716 AXIS_MODES = [
717  ('xyz', 'xyz', 'no swapping'),
718  ('xz-y', 'xz-y', 'ogre standard'),
719  ('-xzy', '-xzy', 'non standard'),
720  ('aldeb', 'aldeb', 'invert x and z axis'),
721 ]
722 
723 def swap(vec):
724  if CONFIG['SWAP_AXIS'] == 'xyz': return vec
725  elif CONFIG['SWAP_AXIS'] == 'xzy':
726  if len(vec) == 3: return mathutils.Vector( [vec.x, vec.z, vec.y] )
727  elif len(vec) == 4: return mathutils.Quaternion( [ vec.w, vec.x, vec.z, vec.y] )
728  elif CONFIG['SWAP_AXIS'] == '-xzy':
729  if len(vec) == 3: return mathutils.Vector( [-vec.x, vec.z, vec.y] )
730  elif len(vec) == 4: return mathutils.Quaternion( [ vec.w, -vec.x, vec.z, vec.y] )
731  elif CONFIG['SWAP_AXIS'] == 'xz-y':
732  if len(vec) == 3: return mathutils.Vector( [vec.x, vec.z, -vec.y] )
733  elif len(vec) == 4: return mathutils.Quaternion( [ vec.w, vec.x, vec.z, -vec.y] )
734  elif CONFIG['SWAP_AXIS'] == 'aldeb':
735  if len(vec) == 3: return mathutils.Vector( [vec.x, -vec.z, vec.y] )
736  elif len(vec) == 4: return mathutils.Quaternion( [ vec.w, vec.x, -vec.z, vec.y] )
737  else:
738  print( 'unknown swap axis mode', CONFIG['SWAP_AXIS'] )
739  assert 0
740 
741 ## Config
742 
743 CONFIG_PATH = bpy.utils.user_resource('CONFIG', path='scripts', create=True)
744 CONFIG_FILENAME = 'blender2ogre.pickle'
745 CONFIG_FILEPATH = os.path.join(CONFIG_PATH, CONFIG_FILENAME)
746 
747 _CONFIG_DEFAULTS_ALL = {
748  'TUNDRA_STREAMING' : True,
749  'COPY_SHADER_PROGRAMS' : True,
750  'MAX_TEXTURE_SIZE' : 4096,
751  'SWAP_AXIS' : 'xz-y', # ogre standard
752  'ONLY_DEFORMABLE_BONES' : False,
753  'ONLY_KEYFRAMED_BONES' : False,
754  'OGRE_INHERIT_SCALE' : False,
755  'FORCE_IMAGE_FORMAT' : 'NONE',
756  'TOUCH_TEXTURES' : True,
757  'SEP_MATS' : True,
758  'SCENE' : True,
759  'SELONLY' : True,
760  'EXPORT_HIDDEN' : True,
761  'FORCE_CAMERA' : True,
762  'FORCE_LAMPS' : True,
763  'MESH' : True,
764  'MESH_OVERWRITE' : True,
765  'ARM_ANIM' : True,
766  'SHAPE_ANIM' : True,
767  'ARRAY' : True,
768  'MATERIALS' : True,
769  'DDS_MIPS' : True,
770  'TRIM_BONE_WEIGHTS' : 0.01,
771  'lodLevels' : 0,
772  'lodDistance' : 300,
773  'lodPercent' : 40,
774  'nuextremityPoints' : 0,
775  'generateEdgeLists' : False,
776  'generateTangents' : True, # this is now safe - ignored if mesh is missing UVs
777  'tangentSemantic' : 'tangent', # used to default to "uvw" but that doesn't seem to work with anything and breaks shaders
778  'tangentUseParity' : 4,
779  'tangentSplitMirrored' : False,
780  'tangentSplitRotated' : False,
781  'reorganiseBuffers' : True,
782  'optimiseAnimations' : True,
783 }
784 
785 _CONFIG_TAGS_ = 'OGRETOOLS_XML_CONVERTER OGRETOOLS_MESH_MAGICK TUNDRA_ROOT OGRE_MESHY IMAGE_MAGICK_CONVERT NVCOMPRESS NVIDIATOOLS_EXE USER_MATERIALS SHADER_PROGRAMS TUNDRA_STREAMING'.split()
786 
787 ''' todo: Change pretty much all of these windows ones. Make a smarter way of detecting
788  Ogre tools and Tundra from various default folders. Also consider making a installer that
789  ships Ogre cmd line tools to ease the setup steps for end users. '''
790 
791 _CONFIG_DEFAULTS_WINDOWS = {
792  'OGRETOOLS_XML_CONVERTER' : 'C:\\OgreCommandLineTools\\OgreXmlConverter.exe',
793  'OGRETOOLS_MESH_MAGICK' : 'C:\\OgreCommandLineTools\\MeshMagick.exe',
794  'TUNDRA_ROOT' : 'C:\\Tundra2',
795  'OGRE_MESHY' : 'C:\\OgreMeshy\\Ogre Meshy.exe',
796  'IMAGE_MAGICK_CONVERT' : 'C:\\Program Files\\ImageMagick\\convert.exe',
797  'NVIDIATOOLS_EXE' : 'C:\\Program Files\\NVIDIA Corporation\\DDS Utilities\\nvdxt.exe',
798  'USER_MATERIALS' : 'C:\\Tundra2\\media\\materials',
799  'SHADER_PROGRAMS' : 'C:\\Tundra2\\media\\materials\\programs',
800  'NVCOMPRESS' : 'C:\\nvcompress.exe'
801 }
802 
803 _CONFIG_DEFAULTS_UNIX = {
804  'OGRETOOLS_XML_CONVERTER' : '/usr/local/bin/OgreXMLConverter', # source build is better
805  'OGRETOOLS_MESH_MAGICK' : '/usr/local/bin/MeshMagick',
806  'TUNDRA_ROOT' : '~/Tundra2',
807  'OGRE_MESHY' : '~/OgreMeshy/Ogre Meshy.exe',
808  'IMAGE_MAGICK_CONVERT' : '/usr/bin/convert',
809  'NVIDIATOOLS_EXE' : '~/.wine/drive_c/Program Files/NVIDIA Corporation/DDS Utilities',
810  'USER_MATERIALS' : '~/Tundra2/media/materials',
811  'SHADER_PROGRAMS' : '~/Tundra2/media/materials/programs',
812  #'USER_MATERIALS' : '~/ogre_src_v1-7-3/Samples/Media/materials',
813  #'SHADER_PROGRAMS' : '~/ogre_src_v1-7-3/Samples/Media/materials/programs',
814  'NVCOMPRESS' : '/usr/local/bin/nvcompress'
815 }
816 
817 # Unix: Replace ~ with absolute home dir path
818 if sys.platform.startswith('linux') or sys.platform.startswith('darwin') or sys.platform.startswith('freebsd'):
819  for tag in _CONFIG_DEFAULTS_UNIX:
820  path = _CONFIG_DEFAULTS_UNIX[ tag ]
821  if path.startswith('~'):
822  _CONFIG_DEFAULTS_UNIX[ tag ] = os.path.expanduser( path )
823  elif tag.startswith('OGRETOOLS') and not os.path.isfile( path ):
824  _CONFIG_DEFAULTS_UNIX[ tag ] = os.path.join( '/usr/bin', os.path.split( path )[-1] )
825  del tag
826  del path
827 
828 
829 ## PUBLIC API continues
830 
831 CONFIG = {}
832 
833 def load_config():
834  global CONFIG
835 
836  if os.path.isfile( CONFIG_FILEPATH ):
837  try:
838  with open( CONFIG_FILEPATH, 'rb' ) as f:
839  CONFIG = pickle.load( f )
840  except:
841  print('[ERROR]: Can not read config from %s' %CONFIG_FILEPATH)
842 
843  for tag in _CONFIG_DEFAULTS_ALL:
844  if tag not in CONFIG:
845  CONFIG[ tag ] = _CONFIG_DEFAULTS_ALL[ tag ]
846 
847  for tag in _CONFIG_TAGS_:
848  if tag not in CONFIG:
849  if sys.platform.startswith('win'):
850  CONFIG[ tag ] = _CONFIG_DEFAULTS_WINDOWS[ tag ]
851  elif sys.platform.startswith('linux') or sys.platform.startswith('darwin') or sys.platform.startswith('freebsd'):
852  CONFIG[ tag ] = _CONFIG_DEFAULTS_UNIX[ tag ]
853  else:
854  print( 'ERROR: unknown platform' )
855  assert 0
856 
857  try:
858  if sys.platform.startswith('win'):
859  import winreg
860  # Find the blender2ogre install path from windows registry
861  registry_key = winreg.OpenKey(winreg.HKEY_CLASSES_ROOT, r'Software\blender2ogre', 0, winreg.KEY_READ)
862  exe_install_dir = winreg.QueryValueEx(registry_key, "Path")[0]
863  if exe_install_dir != "":
864  # OgreXmlConverter
865  if os.path.isfile(exe_install_dir + "OgreXmlConverter.exe"):
866  print ("Using OgreXmlConverter from install path:", exe_install_dir + "OgreXmlConverter.exe")
867  CONFIG['OGRETOOLS_XML_CONVERTER'] = exe_install_dir + "OgreXmlConverter.exe"
868  # Run auto updater as silent. Notifies user if there is a new version out.
869  # This will not show any UI if there are no update and will go to network
870  # only once per 2 days so it wont be spending much resources either.
871  # todo: Move this to a more appropriate place than load_config()
872  if os.path.isfile(exe_install_dir + "check-for-updates.exe"):
873  subprocess.Popen([exe_install_dir + "check-for-updates.exe", "/silent"])
874  except Exception as e:
875  print("Exception while reading windows registry:", e)
876 
877  # Setup temp hidden RNA to expose the file paths
878  for tag in _CONFIG_TAGS_:
879  default = CONFIG[ tag ]
880  func = eval( 'lambda self,con: CONFIG.update( {"%s" : self.%s} )' %(tag,tag) )
881  if type(default) is bool:
882  prop = BoolProperty(
883  name=tag, description='updates bool setting', default=default,
884  options={'SKIP_SAVE'}, update=func
885  )
886  else:
887  prop = StringProperty(
888  name=tag, description='updates path setting', maxlen=128, default=default,
889  options={'SKIP_SAVE'}, update=func
890  )
891  setattr( bpy.types.WindowManager, tag, prop )
892 
893  return CONFIG
894 
895 CONFIG = load_config()
896 
897 def save_config():
898  #for key in CONFIG: print( '%s = %s' %(key, CONFIG[key]) )
899  if os.path.isdir( CONFIG_PATH ):
900  try:
901  with open( CONFIG_FILEPATH, 'wb' ) as f:
902  pickle.dump( CONFIG, f, -1 )
903  except:
904  print('[ERROR]: Can not write to %s' %CONFIG_FILEPATH)
905  else:
906  print('[ERROR:] Config directory does not exist %s' %CONFIG_PATH)
907 
908 class Blender2Ogre_ConfigOp(bpy.types.Operator):
909  '''operator: saves current b2ogre configuration'''
910  bl_idname = "ogre.save_config"
911  bl_label = "save config file"
912  bl_options = {'REGISTER'}
913 
914  @classmethod
915  def poll(cls, context):
916  return True
917  def invoke(self, context, event):
918  save_config()
919  Report.reset()
920  Report.messages.append('SAVED %s' %CONFIG_FILEPATH)
921  Report.show()
922  return {'FINISHED'}
923 
924 
925 # Make default material for missing materials:
926 # * Red flags for users so they can quickly see what they forgot to assign a material to.
927 # * Do not crash if no material on object - thats annoying for the user.
928 
929 MISSING_MATERIAL = '''
930 material _missing_material_
931 {
932  receive_shadows off
933  technique
934  {
935  pass
936  {
937  ambient 0.1 0.1 0.1 1.0
938  diffuse 0.8 0.0 0.0 1.0
939  specular 0.5 0.5 0.5 1.0 12.5
940  emissive 0.3 0.3 0.3 1.0
941  }
942  }
943 }
944 '''
945 
946 ## Helper functions
947 
948 def timer_diff_str(start):
949  return "%0.2f" % (time.time()-start)
950 
951 def find_bone_index( ob, arm, groupidx): # sometimes the groups are out of order, this finds the right index.
952  if groupidx < len(ob.vertex_groups): # reported by Slacker
953  vg = ob.vertex_groups[ groupidx ]
954  j = 0
955  for i,bone in enumerate(arm.pose.bones):
956  if not bone.bone.use_deform and CONFIG['ONLY_DEFORMABLE_BONES']:
957  j+=1 # if we skip bones we need to adjust the id
958  if bone.name == vg.name:
959  return i-j
960  else:
961  print('WARNING: object vertex groups not in sync with armature', ob, arm, groupidx)
962 
963 def mesh_is_smooth( mesh ):
964  for face in mesh.tessfaces:
965  if face.use_smooth: return True
966 
967 def find_uv_layer_index( uvname, material=None ):
968  # This breaks if users have uv layers with same name with different indices over different objects
969  idx = 0
970  for mesh in bpy.data.meshes:
971  if material is None or material.name in mesh.materials:
972  if mesh.uv_textures:
973  names = [ uv.name for uv in mesh.uv_textures ]
974  if uvname in names:
975  idx = names.index( uvname )
976  break # should we check all objects using material and enforce the same index?
977  return idx
978 
979 def has_custom_property( a, name ):
980  for prop in a.items():
981  n,val = prop
982  if n == name:
983  return True
984 
985 def is_strictly_simple_terrain( ob ):
986  # A default plane, with simple-subsurf and displace modifier on Z
987  if len(ob.data.vertices) != 4 and len(ob.data.tessfaces) != 1:
988  return False
989  elif len(ob.modifiers) < 2:
990  return False
991  elif ob.modifiers[0].type != 'SUBSURF' or ob.modifiers[1].type != 'DISPLACE':
992  return False
993  elif ob.modifiers[0].subdivision_type != 'SIMPLE':
994  return False
995  elif ob.modifiers[1].direction != 'Z':
996  return False # disallow NORMAL and other modes
997  else:
998  return True
999 
1000 def get_image_textures( mat ):
1001  r = []
1002  for s in mat.texture_slots:
1003  if s and s.texture.type == 'IMAGE':
1004  r.append( s )
1005  return r
1006 
1007 def indent( level, *args ):
1008  if not args:
1009  return ' ' * level
1010  else:
1011  a = ''
1012  for line in args:
1013  a += ' ' * level
1014  a += line
1015  a += '\n'
1016  return a
1017 
1018 def gather_instances():
1019  instances = {}
1020  for ob in bpy.context.scene.objects:
1021  if ob.data and ob.data.users > 1:
1022  if ob.data not in instances:
1023  instances[ ob.data ] = []
1024  instances[ ob.data ].append( ob )
1025  return instances
1026 
1027 def select_instances( context, name ):
1028  for ob in bpy.context.scene.objects:
1029  ob.select = False
1030  ob = bpy.context.scene.objects[ name ]
1031  if ob.data:
1032  inst = gather_instances()
1033  for ob in inst[ ob.data ]: ob.select = True
1034  bpy.context.scene.objects.active = ob
1035 
1036 def select_group( context, name, options={} ):
1037  for ob in bpy.context.scene.objects:
1038  ob.select = False
1039  for grp in bpy.data.groups:
1040  if grp.name == name:
1041  # context.scene.objects.active = grp.objects
1042  # Note that the context is read-only. These values cannot be modified directly,
1043  # though they may be changed by running API functions or by using the data API.
1044  # So bpy.context.object = obj will raise an error. But bpy.context.scene.objects.active = obj
1045  # will work as expected. - http://wiki.blender.org/index.php?title=Dev:2.5/Py/API/Intro&useskin=monobook
1046  bpy.context.scene.objects.active = grp.objects[0]
1047  for ob in grp.objects:
1048  ob.select = True
1049  else:
1050  pass
1051 
1052 def get_objects_using_materials( mats ):
1053  obs = []
1054  for ob in bpy.data.objects:
1055  if ob.type == 'MESH':
1056  for mat in ob.data.materials:
1057  if mat in mats:
1058  if ob not in obs:
1059  obs.append( ob )
1060  break
1061  return obs
1062 
1063 def get_materials_using_image( img ):
1064  mats = []
1065  for mat in bpy.data.materials:
1066  for slot in get_image_textures( mat ):
1067  if slot.texture.image == img:
1068  if mat not in mats:
1069  mats.append( mat )
1070  return mats
1071 
1072 def get_parent_matrix( ob, objects ):
1073  if not ob.parent:
1074  return mathutils.Matrix(((1,0,0,0),(0,1,0,0),(0,0,1,0),(0,0,0,1))) # Requiered for Blender SVN > 2.56
1075  else:
1076  if ob.parent in objects:
1077  return ob.parent.matrix_world.copy()
1078  else:
1079  return get_parent_matrix(ob.parent, objects)
1080 
1081 def merge_group( group ):
1082  print('--------------- merge group ->', group )
1083  copies = []
1084  for ob in group.objects:
1085  if ob.type == 'MESH':
1086  print( '\t group member', ob.name )
1087  o2 = ob.copy(); copies.append( o2 )
1088  o2.data = o2.to_mesh(bpy.context.scene, True, "PREVIEW") # collaspe modifiers
1089  while o2.modifiers:
1090  o2.modifiers.remove( o2.modifiers[0] )
1091  bpy.context.scene.objects.link( o2 ) #; o2.select = True
1092  merged = merge( copies )
1093  merged.name = group.name
1094  merged.data.name = group.name
1095  return merged
1096 
1097 def merge_objects( objects, name='_temp_', transform=None ):
1098  assert objects
1099  copies = []
1100  for ob in objects:
1101  ob.select = False
1102  if ob.type == 'MESH':
1103  o2 = ob.copy(); copies.append( o2 )
1104  o2.data = o2.to_mesh(bpy.context.scene, True, "PREVIEW") # collaspe modifiers
1105  while o2.modifiers:
1106  o2.modifiers.remove( o2.modifiers[0] )
1107  if transform:
1108  o2.matrix_world = transform * o2.matrix_local
1109  bpy.context.scene.objects.link( o2 ) #; o2.select = True
1110  merged = merge( copies )
1111  merged.name = name
1112  merged.data.name = name
1113  return merged
1114 
1115 def merge( objects ):
1116  print('MERGE', objects)
1117  for ob in bpy.context.selected_objects:
1118  ob.select = False
1119  for ob in objects:
1120  print('\t'+ob.name)
1121  ob.select = True
1122  assert not ob.library
1123  bpy.context.scene.objects.active = ob
1124  bpy.ops.object.join()
1125  return bpy.context.active_object
1126 
1127 def get_merge_group( ob, prefix='merge' ):
1128  m = []
1129  for grp in ob.users_group:
1130  if grp.name.lower().startswith(prefix): m.append( grp )
1131  if len(m)==1:
1132  #if ob.data.users != 1:
1133  # print( 'WARNING: an instance can not be in a merge group' )
1134  # return
1135  return m[0]
1136  elif m:
1137  print('WARNING: an object can not be in two merge groups at the same time', ob)
1138  return
1139 
1140 def wordwrap( txt ):
1141  r = ['']
1142  for word in txt.split(' '): # do not split on tabs
1143  word = word.replace('\t', ' '*3)
1144  r[-1] += word + ' '
1145  if len(r[-1]) > 90:
1146  r.append( '' )
1147  return r
1148 
1149 ## RPython xml dom
1150 
1151 class RElement(object):
1152  def appendChild( self, child ):
1153  self.childNodes.append( child )
1154 
1155  def setAttribute( self, name, value ):
1156  self.attributes[name]=value
1157 
1158  def __init__(self, tag):
1159  self.tagName = tag
1160  self.childNodes = []
1161  self.attributes = {}
1162 
1163  def toprettyxml(self, lines, indent ):
1164  s = '<%s ' % self.tagName
1165  sortedNames = sorted( self.attributes.keys() )
1166  for name in sortedNames:
1167  value = self.attributes[name]
1168  if not isinstance(value, str):
1169  value = str(value)
1170  s += '%s=%s ' % (name, quoteattr(value))
1171  if not self.childNodes:
1172  s += '/>'; lines.append( (' '*indent)+s )
1173  else:
1174  s += '>'; lines.append( (' '*indent)+s )
1175  indent += 1
1176  for child in self.childNodes:
1177  child.toprettyxml( lines, indent )
1178  indent -= 1
1179  lines.append((' '*indent) + '</%s>' % self.tagName )
1180 
1181 class RDocument(object):
1182  def __init__(self):
1183  self.documentElement = None
1184 
1185  def appendChild(self, root):
1186  self.documentElement = root
1187 
1188  def createElement(self, tag):
1189  e = RElement(tag)
1190  e.document = self
1191  return e
1192 
1193  def toprettyxml(self):
1194  indent = 0
1195  lines = []
1196  self.documentElement.toprettyxml(lines, indent)
1197  return '\n'.join(lines)
1198 
1199 class SimpleSaxWriter():
1200  def __init__(self, output, root_tag, root_attrs):
1201  self.output = output
1202  self.root_tag = root_tag
1203  self.indent=0
1204  output.write("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n")
1205  self.start_tag(root_tag, root_attrs)
1206 
1207  def _out_tag(self, name, attrs, isLeaf):
1208  # sorted attributes -- don't want attributes output in random order, which is what the XMLGenerator class does
1209  self.output.write(" " * self.indent)
1210  self.output.write("<%s" % name)
1211  sortedNames = sorted( attrs.keys() ) # sorted list of attribute names
1212  for name in sortedNames:
1213  value = attrs[ name ]
1214  # if not of type string,
1215  if not isinstance(value, str):
1216  # turn it into a string
1217  value = str(value)
1218  self.output.write(" %s=%s" % (name, quoteattr(value)))
1219  if isLeaf:
1220  self.output.write("/")
1221  else:
1222  self.indent += 4
1223  self.output.write(">\n")
1224 
1225  def start_tag(self, name, attrs):
1226  self._out_tag(name, attrs, False)
1227 
1228  def end_tag(self, name):
1229  self.indent -= 4
1230  self.output.write(" " * self.indent)
1231  self.output.write("</%s>\n" % name)
1232 
1233  def leaf_tag(self, name, attrs):
1234  self._out_tag(name, attrs, True)
1235 
1236  def close(self):
1237  self.end_tag( self.root_tag )
1238 
1239 ## Report Hack
1240 
1241 class ReportSingleton(object):
1242  def __init__(self):
1243  self.reset()
1244 
1245  def show(self):
1246  bpy.ops.wm.call_menu( name='MiniReport' )
1247 
1248  def reset(self):
1249  self.materials = []
1250  self.meshes = []
1251  self.lights = []
1252  self.cameras = []
1253  self.armatures = []
1254  self.armature_animations = []
1255  self.shape_animations = []
1256  self.textures = []
1257  self.vertices = 0
1258  self.orig_vertices = 0
1259  self.faces = 0
1260  self.triangles = 0
1261  self.warnings = []
1262  self.errors = []
1263  self.messages = []
1264  self.paths = []
1265 
1266  def report(self):
1267  r = ['Report:']
1268  ex = ['Extended Report:']
1269  if self.errors:
1270  r.append( ' ERRORS:' )
1271  for a in self.errors: r.append( ' - %s' %a )
1272 
1273  #if not bpy.context.selected_objects:
1274  # self.warnings.append('YOU DID NOT SELECT ANYTHING TO EXPORT')
1275  if self.warnings:
1276  r.append( ' WARNINGS:' )
1277  for a in self.warnings: r.append( ' - %s' %a )
1278 
1279  if self.messages:
1280  r.append( ' MESSAGES:' )
1281  for a in self.messages: r.append( ' - %s' %a )
1282  if self.paths:
1283  r.append( ' PATHS:' )
1284  for a in self.paths: r.append( ' - %s' %a )
1285 
1286  if self.vertices:
1287  r.append( ' Original Vertices: %s' %self.orig_vertices)
1288  r.append( ' Exported Vertices: %s' %self.vertices )
1289  r.append( ' Original Faces: %s' %self.faces )
1290  r.append( ' Exported Triangles: %s' %self.triangles )
1291  ## TODO report file sizes, meshes and textures
1292 
1293  for tag in 'meshes lights cameras armatures armature_animations shape_animations materials textures'.split():
1294  attr = getattr(self, tag)
1295  if attr:
1296  name = tag.replace('_',' ').upper()
1297  r.append( ' %s: %s' %(name, len(attr)) )
1298  if attr:
1299  ex.append( ' %s:' %name )
1300  for a in attr: ex.append( ' . %s' %a )
1301 
1302  txt = '\n'.join( r )
1303  ex = '\n'.join( ex ) # console only - extended report
1304  print('_'*80)
1305  print(txt)
1306  print(ex)
1307  print('_'*80)
1308  return txt
1309 
1310 Report = ReportSingleton()
1311 
1312 class MiniReport(bpy.types.Menu):
1313  bl_label = "Mini-Report | (see console for full report)"
1314  def draw(self, context):
1315  layout = self.layout
1316  txt = Report.report()
1317  for line in txt.splitlines():
1318  layout.label(text=line)
1319 
1320 ## RPython xml dom ends
1321 
1322 def _get_proxy_decimate_mod( ob ):
1323  proxy = None
1324  for child in ob.children:
1325  if child.subcollision and child.name.startswith('DECIMATED'):
1326  for mod in child.modifiers:
1327  if mod.type == 'DECIMATE':
1328  return mod
1329 
1330 def bake_terrain( ob, normalize=True ):
1331  assert ob.collision_mode == 'TERRAIN'
1332  terrain = None
1333  for child in ob.children:
1334  if child.subcollision and child.name.startswith('TERRAIN'):
1335  terrain = child
1336  break
1337  assert terrain
1338  data = terrain.to_mesh(bpy.context.scene, True, "PREVIEW")
1339  raw = [ v.co.z for v in data.vertices ]
1340  Zmin = min( raw )
1341  Zmax = max( raw )
1342  depth = Zmax-Zmin
1343  m = 1.0 / depth
1344 
1345  rows = []
1346  i = 0
1347  for x in range( ob.collision_terrain_x_steps ):
1348  row = []
1349  for y in range( ob.collision_terrain_y_steps ):
1350  v = data.vertices[ i ]
1351  if normalize:
1352  z = (v.co.z - Zmin) * m
1353  else:
1354  z = v.co.z
1355  row.append( z )
1356  i += 1
1357  if x%2:
1358  row.reverse() # blender grid prim zig-zags
1359  rows.append( row )
1360  return {'data':rows, 'min':Zmin, 'max':Zmax, 'depth':depth}
1361 
1362 def save_terrain_as_NTF( path, ob ): # Tundra format - hardcoded 16x16 patch format
1363  info = bake_terrain( ob )
1364  url = os.path.join( path, '%s.ntf' % clean_object_name(ob.data.name) )
1365  f = open(url, "wb")
1366  # Header
1367  buf = array.array("I")
1368  xs = ob.collision_terrain_x_steps
1369  ys = ob.collision_terrain_y_steps
1370  xpatches = int(xs/16)
1371  ypatches = int(ys/16)
1372  header = [ xpatches, ypatches ]
1373  buf.fromlist( header )
1374  buf.tofile(f)
1375  # Body
1376  rows = info['data']
1377  for x in range( xpatches ):
1378  for y in range( ypatches ):
1379  patch = []
1380  for i in range(16):
1381  for j in range(16):
1382  v = rows[ (x*16)+i ][ (y*16)+j ]
1383  patch.append( v )
1384  buf = array.array("f")
1385  buf.fromlist( patch )
1386  buf.tofile(f)
1387  f.close()
1388  path,name = os.path.split(url)
1389  R = {
1390  'url':url, 'min':info['min'], 'max':info['max'], 'path':path, 'name':name,
1391  'xpatches': xpatches, 'ypatches': ypatches,
1392  'depth':info['depth'],
1393  }
1394  return R
1395 
1396 class OgreCollisionOp(bpy.types.Operator):
1397  '''Ogre Collision'''
1398  bl_idname = "ogre.set_collision"
1399  bl_label = "modify collision"
1400  bl_options = {'REGISTER'}
1401  MODE = StringProperty(name="toggle mode", maxlen=32, default="disable")
1402 
1403  @classmethod
1404  def poll(cls, context):
1405  if context.active_object and context.active_object.type == 'MESH':
1406  return True
1407 
1408  def get_subcollisions( self, ob, create=True ):
1409  r = get_subcollisions( ob )
1410  if not r and create:
1411  method = getattr(self, 'create_%s'%ob.collision_mode)
1412  p = method(ob)
1413  p.name = '%s.%s' %(ob.collision_mode, ob.name)
1414  p.subcollision = True
1415  r.append( p )
1416  return r
1417 
1418  def create_DECIMATED(self, ob):
1419  child = ob.copy()
1420  bpy.context.scene.objects.link( child )
1421  child.matrix_local = mathutils.Matrix()
1422  child.parent = ob
1423  child.hide_select = True
1424  child.draw_type = 'WIRE'
1425  #child.select = False
1426  child.lock_location = [True]*3
1427  child.lock_rotation = [True]*3
1428  child.lock_scale = [True]*3
1429  decmod = child.modifiers.new('proxy', type='DECIMATE')
1430  decmod.ratio = 0.5
1431  return child
1432 
1433  def create_TERRAIN(self, ob):
1434  x = ob.collision_terrain_x_steps
1435  y = ob.collision_terrain_y_steps
1436  #################################
1437  #pos = ob.matrix_world.to_translation()
1438  bpy.ops.mesh.primitive_grid_add(
1439  x_subdivisions=x,
1440  y_subdivisions=y,
1441  size=1.0 ) #, location=pos )
1442  grid = bpy.context.active_object
1443  assert grid.name.startswith('Grid')
1444  grid.collision_terrain_x_steps = x
1445  grid.collision_terrain_y_steps = y
1446  #############################
1447  x,y,z = ob.dimensions
1448  sx,sy,sz = ob.scale
1449  x *= 1.0/sx
1450  y *= 1.0/sy
1451  z *= 1.0/sz
1452  grid.scale.x = x/2
1453  grid.scale.y = y/2
1454  grid.location.z -= z/2
1455  grid.data.show_all_edges = True
1456  grid.draw_type = 'WIRE'
1457  grid.hide_select = True
1458  #grid.select = False
1459  grid.lock_location = [True]*3
1460  grid.lock_rotation = [True]*3
1461  grid.lock_scale = [True]*3
1462  grid.parent = ob
1463  bpy.context.scene.objects.active = ob
1464  mod = grid.modifiers.new(name='temp', type='SHRINKWRAP')
1465  mod.wrap_method = 'PROJECT'
1466  mod.use_project_z = True
1467  mod.target = ob
1468  mod.cull_face = 'FRONT'
1469  return grid
1470 
1471  def invoke(self, context, event):
1472  ob = context.active_object
1473  game = ob.game
1474  subtype = None
1475 
1476  if ':' in self.MODE:
1477  mode, subtype = self.MODE.split(':')
1478  ##BLENDERBUG##ob.game.collision_bounds_type = subtype # BUG this can not come before
1479  if subtype in 'BOX SPHERE CYLINDER CONE CAPSULE'.split():
1480  ob.draw_bounds_type = subtype
1481  else:
1482  ob.draw_bounds_type = 'POLYHEDRON'
1483  ob.game.collision_bounds_type = subtype # BLENDERBUG - this must come after draw_bounds_type assignment
1484  else:
1485  mode = self.MODE
1486  ob.collision_mode = mode
1487 
1488  if ob.data.show_all_edges:
1489  ob.data.show_all_edges = False
1490  if ob.show_texture_space:
1491  ob.show_texture_space = False
1492  if ob.show_bounds:
1493  ob.show_bounds = False
1494  if ob.show_wire:
1495  ob.show_wire = False
1496  for child in ob.children:
1497  if child.subcollision and not child.hide:
1498  child.hide = True
1499 
1500  if mode == 'NONE':
1501  game.use_ghost = True
1502  game.use_collision_bounds = False
1503  elif mode == 'PRIMITIVE':
1504  game.use_ghost = False
1505  game.use_collision_bounds = True
1506  ob.show_bounds = True
1507  elif mode == 'MESH':
1508  game.use_ghost = False
1509  game.use_collision_bounds = True
1510  ob.show_wire = True
1511  if game.collision_bounds_type == 'CONVEX_HULL':
1512  ob.show_texture_space = True
1513  else:
1514  ob.data.show_all_edges = True
1515  elif mode == 'DECIMATED':
1516  game.use_ghost = True
1517  game.use_collision_bounds = False
1518  game.use_collision_compound = True
1519  proxy = self.get_subcollisions(ob)[0]
1520  if proxy.hide: proxy.hide = False
1521  ob.game.use_collision_compound = True # proxy
1522  mod = _get_proxy_decimate_mod( ob )
1523  mod.show_viewport = True
1524  if not proxy.select: # ugly (but works)
1525  proxy.hide_select = False
1526  proxy.select = True
1527  proxy.hide_select = True
1528  if game.collision_bounds_type == 'CONVEX_HULL':
1529  ob.show_texture_space = True
1530  elif mode == 'TERRAIN':
1531  game.use_ghost = True
1532  game.use_collision_bounds = False
1533  game.use_collision_compound = True
1534  proxy = self.get_subcollisions(ob)[0]
1535  if proxy.hide:
1536  proxy.hide = False
1537  elif mode == 'COMPOUND':
1538  game.use_ghost = True
1539  game.use_collision_bounds = False
1540  game.use_collision_compound = True
1541  else:
1542  assert 0 # unknown mode
1543 
1544  return {'FINISHED'}
1545 
1546 # UI panels
1547 
1548 @UI
1549 class PANEL_Physics(bpy.types.Panel):
1550  bl_space_type = 'VIEW_3D'
1551  bl_region_type = 'UI'
1552  bl_label = "Physics"
1553 
1554  @classmethod
1555  def poll(cls, context):
1556  if context.active_object:
1557  return True
1558  else:
1559  return False
1560 
1561  def draw(self, context):
1562  layout = self.layout
1563  ob = context.active_object
1564  game = ob.game
1565 
1566  if ob.type != 'MESH':
1567  return
1568  elif ob.subcollision == True:
1569  box = layout.box()
1570  if ob.parent:
1571  box.label(text='object is a collision proxy for: %s' %ob.parent.name)
1572  else:
1573  box.label(text='WARNING: collision proxy missing parent')
1574  return
1575 
1576  box = layout.box()
1577  box.prop(ob, 'physics_mode')
1578  if ob.physics_mode != 'NONE':
1579  box.prop(game, 'mass', text='Mass')
1580  box.prop(ob, 'physics_friction', text='Friction', slider=True)
1581  box.prop(ob, 'physics_bounce', text='Bounce', slider=True)
1582 
1583  box.label(text="Damping:")
1584  box.prop(game, 'damping', text='Translation', slider=True)
1585  box.prop(game, 'rotation_damping', text='Rotation', slider=True)
1586 
1587  box.label(text="Velocity:")
1588  box.prop(game, "velocity_min", text="Minimum")
1589  box.prop(game, "velocity_max", text="Maximum")
1590 
1591 @UI
1592 class PANEL_Collision(bpy.types.Panel):
1593  bl_space_type = 'VIEW_3D'
1594  bl_region_type = 'UI'
1595  bl_label = "Collision"
1596 
1597  @classmethod
1598  def poll(cls, context):
1599  if context.active_object:
1600  return True
1601  else:
1602  return False
1603 
1604  def draw(self, context):
1605  layout = self.layout
1606  ob = context.active_object
1607  game = ob.game
1608 
1609  if ob.type != 'MESH':
1610  return
1611  elif ob.subcollision == True:
1612  box = layout.box()
1613  if ob.parent:
1614  box.label(text='object is a collision proxy for: %s' %ob.parent.name)
1615  else:
1616  box.label(text='WARNING: collision proxy missing parent')
1617  return
1618 
1619  mode = ob.collision_mode
1620  if mode == 'NONE':
1621  box = layout.box()
1622  op = box.operator( 'ogre.set_collision', text='Enable Collision', icon='PHYSICS' )
1623  op.MODE = 'PRIMITIVE:%s' %game.collision_bounds_type
1624  else:
1625  prim = game.collision_bounds_type
1626 
1627  box = layout.box()
1628  op = box.operator( 'ogre.set_collision', text='Disable Collision', icon='X' )
1629  op.MODE = 'NONE'
1630  box.prop(game, "collision_margin", text="Collision Margin", slider=True)
1631 
1632  box = layout.box()
1633  if mode == 'PRIMITIVE':
1634  box.label(text='Primitive: %s' %prim)
1635  else:
1636  box.label(text='Primitive')
1637 
1638  row = box.row()
1639  _icons = {
1640  'BOX':'MESH_CUBE', 'SPHERE':'MESH_UVSPHERE', 'CYLINDER':'MESH_CYLINDER',
1641  'CONE':'MESH_CONE', 'CAPSULE':'META_CAPSULE'}
1642  for a in 'BOX SPHERE CYLINDER CONE CAPSULE'.split():
1643  if prim == a and mode == 'PRIMITIVE':
1644  op = row.operator( 'ogre.set_collision', text='', icon=_icons[a], emboss=True )
1645  op.MODE = 'PRIMITIVE:%s' %a
1646  else:
1647  op = row.operator( 'ogre.set_collision', text='', icon=_icons[a], emboss=False )
1648  op.MODE = 'PRIMITIVE:%s' %a
1649 
1650  box = layout.box()
1651  if mode == 'MESH': box.label(text='Mesh: %s' %prim.split('_')[0] )
1652  else: box.label(text='Mesh')
1653  row = box.row()
1654  row.label(text='- - - - - - - - - - - - - -')
1655  _icons = {'TRIANGLE_MESH':'MESH_ICOSPHERE', 'CONVEX_HULL':'SURFACE_NCURVE'}
1656  for a in 'TRIANGLE_MESH CONVEX_HULL'.split():
1657  if prim == a and mode == 'MESH':
1658  op = row.operator( 'ogre.set_collision', text='', icon=_icons[a], emboss=True )
1659  op.MODE = 'MESH:%s' %a
1660  else:
1661  op = row.operator( 'ogre.set_collision', text='', icon=_icons[a], emboss=False )
1662  op.MODE = 'MESH:%s' %a
1663 
1664  box = layout.box()
1665  if mode == 'DECIMATED':
1666  box.label(text='Decimate: %s' %prim.split('_')[0] )
1667  row = box.row()
1668  mod = _get_proxy_decimate_mod( ob )
1669  assert mod # decimate modifier is missing
1670  row.label(text='Faces: %s' %mod.face_count )
1671  box.prop( mod, 'ratio', text='' )
1672  else:
1673  box.label(text='Decimate')
1674  row = box.row()
1675  row.label(text='- - - - - - - - - - - - - -')
1676 
1677  _icons = {'TRIANGLE_MESH':'MESH_ICOSPHERE', 'CONVEX_HULL':'SURFACE_NCURVE'}
1678  for a in 'TRIANGLE_MESH CONVEX_HULL'.split():
1679  if prim == a and mode == 'DECIMATED':
1680  op = row.operator( 'ogre.set_collision', text='', icon=_icons[a], emboss=True )
1681  op.MODE = 'DECIMATED:%s' %a
1682  else:
1683  op = row.operator( 'ogre.set_collision', text='', icon=_icons[a], emboss=False )
1684  op.MODE = 'DECIMATED:%s' %a
1685 
1686  box = layout.box()
1687  if mode == 'TERRAIN':
1688  terrain = get_subcollisions( ob )[0]
1689  if ob.collision_terrain_x_steps != terrain.collision_terrain_x_steps or ob.collision_terrain_y_steps != terrain.collision_terrain_y_steps:
1690  op = box.operator( 'ogre.set_collision', text='Rebuild Terrain', icon='MESH_GRID' )
1691  op.MODE = 'TERRAIN'
1692  else:
1693  box.label(text='Terrain:')
1694  row = box.row()
1695  row.prop( ob, 'collision_terrain_x_steps', 'X' )
1696  row.prop( ob, 'collision_terrain_y_steps', 'Y' )
1697  #box.prop( terrain.modifiers[0], 'offset' ) # gets normalized away
1698  box.prop( terrain.modifiers[0], 'cull_face', text='Cull' )
1699  box.prop( terrain, 'location' ) # TODO hide X and Y
1700  else:
1701  op = box.operator( 'ogre.set_collision', text='Terrain Collision', icon='MESH_GRID' )
1702  op.MODE = 'TERRAIN'
1703 
1704  box = layout.box()
1705  if mode == 'COMPOUND':
1706  op = box.operator( 'ogre.set_collision', text='Compound Collision', icon='ROTATECOLLECTION' )
1707  else:
1708  op = box.operator( 'ogre.set_collision', text='Compound Collision', icon='ROTATECOLLECTION' )
1709  op.MODE = 'COMPOUND'
1710 
1711 @UI
1712 class PANEL_Configure(bpy.types.Panel):
1713  bl_space_type = 'PROPERTIES'
1714  bl_region_type = 'WINDOW'
1715  bl_context = "scene"
1716  bl_label = "Ogre Configuration File"
1717 
1718  def draw(self, context):
1719  layout = self.layout
1720  op = layout.operator( 'ogre.save_config', text='update config file', icon='FILE' )
1721  for tag in _CONFIG_TAGS_:
1722  layout.prop( context.window_manager, tag )
1723 
1724 
1725 ## Pop up dialog for various info/error messages
1726 
1727 popup_message = ""
1728 
1729 class PopUpDialogOperator(bpy.types.Operator):
1730  bl_idname = "object.popup_dialog_operator"
1731  bl_label = "blender2ogre"
1732 
1733  def __init__(self):
1734  print("dialog Start")
1735 
1736  def __del__(self):
1737  print("dialog End")
1738 
1739  def execute(self, context):
1740  print ("execute")
1741  return {'RUNNING_MODAL'}
1742 
1743  def draw(self, context):
1744  # todo: Make this bigger and center on screen.
1745  # Blender UI stuff seems quite complex, would
1746  # think that showing a dialog with a message thath
1747  # does not hide when mouse is moved would be simpler!
1748  global popup_message
1749  layout = self.layout
1750  col = layout.column()
1751  col.label(popup_message, 'ERROR')
1752 
1753  def invoke(self, context, event):
1754  wm = context.window_manager
1755  wm.invoke_popup(self)
1756  wm.modal_handler_add(self)
1757  return {'RUNNING_MODAL'}
1758 
1759  def modal(self, context, event):
1760  # Close
1761  if event.type == 'LEFTMOUSE':
1762  print ("Left mouse")
1763  return {'FINISHED'}
1764  # Close
1765  elif event.type in ('RIGHTMOUSE', 'ESC'):
1766  print ("right mouse")
1767  return {'FINISHED'}
1768 
1769  print("running modal")
1770  return {'RUNNING_MODAL'}
1771 
1772 def show_dialog(message):
1773  global popup_message
1774  popup_message = message
1775  bpy.ops.object.popup_dialog_operator('INVOKE_DEFAULT')
1776 
1777 ## Game Logic Documentation
1778 
1779 _game_logic_intro_doc_ = '''
1780 Hijacking the BGE
1781 
1782 Blender contains a fully functional game engine (BGE) that is highly useful for learning the concepts of game programming by breaking it down into three simple parts: Sensor, Controller, and Actuator. An Ogre based game engine will likely have similar concepts in its internal API and game logic scripting. Without a custom interface to define game logic, very often game designers may have to resort to having programmers implement their ideas in purely handwritten script. This is prone to breakage because object names then end up being hard-coded. Not only does this lead to non-reusable code, its also a slow process. Why should we have to resort to this when Blender already contains a very rich interface for game logic? By hijacking a subset of the BGE interface we can make this workflow between game designer and game programmer much better.
1783 
1784 The OgreDocScene format can easily be extened to include extra game logic data. While the BGE contains some features that can not be easily mapped to other game engines, there are many are highly useful generic features we can exploit, including many of the Sensors and Actuators. Blender uses the paradigm of: 1. Sensor -> 2. Controller -> 3. Actuator. In pseudo-code, this can be thought of as: 1. on-event -> 2. conditional logic -> 3. do-action. The designer is most often concerned with the on-events (the Sensors), and the do-actions (the Actuators); and the BGE interface provides a clear way for defining and editing those. Its a harder task to provide a good interface for the conditional logic (Controller), that is flexible enough to fit everyones different Ogre engine and requirements, so that is outside the scope of this exporter at this time. A programmer will still be required to fill the gap between Sensor and Actuator, but hopefully his work is greatly reduced and can write more generic/reuseable code.
1785 
1786 The rules for which Sensors trigger which Actuators is left undefined, as explained above we are hijacking the BGE interface not trying to export and reimplement everything. BGE Controllers and all links are ignored by the exporter, so whats the best way to define Sensor/Actuator relationships? One convention that seems logical is to group Sensors and Actuators by name. More complex syntax could be used in Sensor/Actuators names, or they could be completely ignored and instead all the mapping is done by the game programmer using other rules. This issue is not easily solved so designers and the engine programmers will have to decide upon their own conventions, there is no one size fits all solution.
1787 '''
1788 
1789 _ogre_logic_types_doc_ = '''
1790 Supported Sensors:
1791  . Collision
1792  . Near
1793  . Radar
1794  . Touching
1795  . Raycast
1796  . Message
1797 
1798 Supported Actuators:
1799  . Shape Action*
1800  . Edit Object
1801  . Camera
1802  . Constraint
1803  . Message
1804  . Motion
1805  . Sound
1806  . Visibility
1807 
1808 *note: Shape Action
1809 The most common thing a designer will want to do is have an event trigger an animation. The BGE contains an Actuator called "Shape Action", with useful properties like: start/end frame, and blending. It also contains a property called "Action" but this is hidden because the exporter ignores action names and instead uses the names of NLA strips when exporting Ogre animation tracks. The current workaround is to hijack the "Frame Property" attribute and change its name to "animation". The designer can then simply type the name of the animation track (NLA strip). Any custom syntax could actually be implemented here for calling animations, its up to the engine programmer to define how this field will be used. For example: "*.explode" could be implemented to mean "on all objects" play the "explode" animation.
1810 '''
1811 
1812 class _WrapLogic(object):
1813  SwapName = { 'frame_property' : 'animation' } # custom name hacks
1814 
1815  def __init__(self, node):
1816  self.node = node
1817  self.name = node.name
1818  self.type = node.type
1819 
1820  def widget(self, layout):
1821  box = layout.box()
1822  row = box.row()
1823  row.label( text=self.type )
1824  row.separator()
1825  row.prop( self.node, 'name', text='' )
1826  if self.type in self.TYPES:
1827  for name in self.TYPES[ self.type ]:
1828  if name in self.SwapName:
1829  box.prop( self.node, name, text=self.SwapName[name] )
1830  else:
1831  box.prop( self.node, name )
1832 
1833  def xml( self, doc ):
1834  g = doc.createElement( self.LogicType )
1835  g.setAttribute('name', self.name)
1836  g.setAttribute('type', self.type)
1837  if self.type in self.TYPES:
1838  for name in self.TYPES[ self.type ]:
1839  attr = getattr( self.node, name )
1840  if name in self.SwapName: name = self.SwapName[name]
1841  a = doc.createElement( 'component' )
1842  g.appendChild(a)
1843  a.setAttribute('name', name)
1844  if attr is None: a.setAttribute('type', 'POINTER' )
1845  else: a.setAttribute('type', type(attr).__name__)
1846 
1847  if type(attr) in (float, int, str, bool): a.setAttribute('value', str(attr))
1848  elif not attr: a.setAttribute('value', '') # None case
1849  elif hasattr(attr,'filepath'): a.setAttribute('value', attr.filepath)
1850  elif hasattr(attr,'name'): a.setAttribute('value', attr.name)
1851  elif hasattr(attr,'x') and hasattr(attr,'y') and hasattr(attr,'z'):
1852  a.setAttribute('value', '%s %s %s' %(attr.x, attr.y, attr.z))
1853  else:
1854  print('ERROR: unknown type', attr)
1855  return g
1856 
1857 class WrapSensor( _WrapLogic ):
1858  LogicType = 'sensor'
1859  TYPES = {
1860  'COLLISION': ['property'],
1861  'MESSAGE' : ['subject'],
1862  'NEAR' : ['property', 'distance', 'reset_distance'],
1863  'RADAR' : ['property', 'axis', 'angle', 'distance' ],
1864  'RAY' : ['ray_type', 'property', 'material', 'axis', 'range', 'use_x_ray'],
1865  'TOUCH' : ['material'],
1866  }
1867 
1868 class WrapActuator( _WrapLogic ):
1869  LogicType = 'actuator'
1870  TYPES = {
1871  'CAMERA' : ['object', 'height', 'min', 'max', 'axis'],
1872  'CONSTRAINT' : ['mode', 'limit', 'limit_min', 'limit_max', 'damping'],
1873  'MESSAGE' : ['to_property', 'subject', 'body_message'], #skipping body_type
1874  'OBJECT' : 'damping derivate_coefficient force force_max_x force_max_y force_max_z force_min_x force_min_y force_min_z integral_coefficient linear_velocity mode offset_location offset_rotation proportional_coefficient reference_object torque use_local_location use_local_rotation use_local_torque use_servo_limit_x use_servo_limit_y use_servo_limit_z'.split(),
1875  'SOUND' : 'cone_inner_angle_3d cone_outer_angle_3d cone_outer_gain_3d distance_3d_max distance_3d_reference gain_3d_max gain_3d_min mode pitch rolloff_factor_3d sound use_sound_3d volume'.split(), # note .sound contains .filepath
1876  'VISIBILITY' : 'apply_to_children use_occlusion use_visible'.split(),
1877  'SHAPE_ACTION' : 'frame_blend_in frame_end frame_property frame_start mode property use_continue_last_frame'.split(),
1878  'EDIT_OBJECT' : 'dynamic_operation linear_velocity mass mesh mode object time track_object use_3d_tracking use_local_angular_velocity use_local_linear_velocity use_replace_display_mesh use_replace_physics_mesh'.split(),
1879  }
1880 
1881 class _OgreMatPass( object ):
1882  bl_space_type = 'PROPERTIES'
1883  bl_region_type = 'WINDOW'
1884  bl_context = "material"
1885 
1886  @classmethod
1887  def poll(cls, context):
1888  if context.active_object and context.active_object.active_material and context.active_object.active_material.use_material_passes:
1889  return True
1890 
1891  def draw(self, context):
1892  if not hasattr(context, "material"):
1893  return
1894  if not context.active_object:
1895  return
1896  if not context.active_object.active_material:
1897  return
1898 
1899  mat = context.material
1900  ob = context.object
1901  slot = context.material_slot
1902  layout = self.layout
1903  #layout.label(text=str(self.INDEX))
1904  if mat.use_material_passes:
1905  db = layout.box()
1906  nodes = bpyShaders.get_or_create_material_passes( mat )
1907  node = nodes[ self.INDEX ]
1908  split = db.row()
1909  if node.material: split.prop( node.material, 'use_in_ogre_material_pass', text='' )
1910  split.prop( node, 'material' )
1911  if not node.material:
1912  op = split.operator( 'ogre.helper_create_attach_material_layer', icon="PLUS", text='' )
1913  op.INDEX = self.INDEX
1914  if node.material and node.material.use_in_ogre_material_pass:
1915  dbb = db.box()
1916  ogre_material_panel( dbb, node.material, parent=mat )
1917  ogre_material_panel_extra( dbb, node.material )
1918 
1919 class _create_new_material_layer_helper(bpy.types.Operator):
1920  '''helper to create new material layer'''
1921  bl_idname = "ogre.helper_create_attach_material_layer"
1922  bl_label = "creates and assigns new material to layer"
1923  bl_options = {'REGISTER'}
1924  INDEX = IntProperty(name="material layer index", description="index", default=0, min=0, max=8)
1925 
1926  @classmethod
1927  def poll(cls, context):
1928  if context.active_object and context.active_object.active_material and context.active_object.active_material.use_material_passes:
1929  return True
1930 
1931  def execute(self, context):
1932  mat = context.active_object.active_material
1933  nodes = bpyShaders.get_or_create_material_passes( mat )
1934  node = nodes[ self.INDEX ]
1935  node.material = bpy.data.materials.new( name='%s.LAYER%s'%(mat.name,self.INDEX) )
1936  node.material.use_fixed_pipeline = False
1937  node.material.offset_z = (self.INDEX*2) + 2 # nudge each pass by 2
1938  return {'FINISHED'}
1939 
1940 # UI panels continues
1941 
1942 @UI
1943 class PANEL_properties_window_ogre_material( bpy.types.Panel ):
1944  bl_space_type = 'PROPERTIES'
1945  bl_region_type = 'WINDOW'
1946  bl_context = "material"
1947  bl_label = "Ogre Material (base pass)"
1948 
1949  @classmethod
1950  def poll( self, context ):
1951  if not hasattr(context, "material"): return False
1952  if not context.active_object: return False
1953  if not context.active_object.active_material: return False
1954  return True
1955 
1956  def draw(self, context):
1957  mat = context.material
1958  ob = context.object
1959  slot = context.material_slot
1960  layout = self.layout
1961  if not mat.use_material_passes:
1962  box = layout.box()
1963  box.operator( 'ogre.force_setup_material_passes', text="Ogre Material Layers", icon='SCENE_DATA' )
1964 
1965  ogre_material_panel( layout, mat )
1966  ogre_material_panel_extra( layout, mat )
1967 
1968 @UI
1969 class MatPass1( _OgreMatPass, bpy.types.Panel ): INDEX = 0; bl_label = "Ogre Material (pass%s)"%str(INDEX+1)
1970 @UI
1971 class MatPass2( _OgreMatPass, bpy.types.Panel ): INDEX = 1; bl_label = "Ogre Material (pass%s)"%str(INDEX+1)
1972 @UI
1973 class MatPass3( _OgreMatPass, bpy.types.Panel ): INDEX = 2; bl_label = "Ogre Material (pass%s)"%str(INDEX+1)
1974 @UI
1975 class MatPass4( _OgreMatPass, bpy.types.Panel ): INDEX = 3; bl_label = "Ogre Material (pass%s)"%str(INDEX+1)
1976 @UI
1977 class MatPass5( _OgreMatPass, bpy.types.Panel ): INDEX = 4; bl_label = "Ogre Material (pass%s)"%str(INDEX+1)
1978 @UI
1979 class MatPass6( _OgreMatPass, bpy.types.Panel ): INDEX = 5; bl_label = "Ogre Material (pass%s)"%str(INDEX+1)
1980 @UI
1981 class MatPass7( _OgreMatPass, bpy.types.Panel ): INDEX = 6; bl_label = "Ogre Material (pass%s)"%str(INDEX+1)
1982 @UI
1983 class MatPass8( _OgreMatPass, bpy.types.Panel ): INDEX = 7; bl_label = "Ogre Material (pass%s)"%str(INDEX+1)
1984 
1985 @UI
1986 class PANEL_Textures(bpy.types.Panel):
1987  bl_space_type = 'PROPERTIES'
1988  bl_region_type = 'WINDOW'
1989  bl_context = "texture"
1990  bl_label = "Ogre Texture"
1991 
1992  @classmethod
1993  def poll(cls, context):
1994  if not hasattr(context, "texture_slot"):
1995  return False
1996  else: return True
1997 
1998  def draw(self, context):
1999  #if not hasattr(context, "texture_slot"):
2000  # return False
2001  layout = self.layout
2002  #idblock = context_tex_datablock(context)
2003  slot = context.texture_slot
2004  if not slot or not slot.texture:
2005  return
2006 
2007  btype = slot.blend_type # todo: fix this hack if/when slots support pyRNA
2008  ex = False; texop = None
2009  if btype in TextureUnit.colour_op:
2010  if btype=='MIX' and slot.use_map_alpha and not slot.use_stencil:
2011  if slot.diffuse_color_factor >= 1.0:
2012  texop = 'alpha_blend'
2013  else:
2014  texop = TextureUnit.colour_op_ex[ btype ]
2015  ex = True
2016  elif btype=='MIX' and slot.use_map_alpha and slot.use_stencil:
2017  texop = 'blend_current_alpha'; ex=True
2018  elif btype=='MIX' and not slot.use_map_alpha and slot.use_stencil:
2019  texop = 'blend_texture_alpha'; ex=True
2020  else:
2021  texop = TextureUnit.colour_op[ btype ]
2022  elif btype in TextureUnit.colour_op_ex:
2023  texop = TextureUnit.colour_op_ex[ btype ]
2024  ex = True
2025 
2026  box = layout.box()
2027  row = box.row()
2028  if texop:
2029  if ex:
2030  row.prop(slot, "blend_type", text=texop, icon='NEW')
2031  else:
2032  row.prop(slot, "blend_type", text=texop)
2033  else:
2034  row.prop(slot, "blend_type", text='(invalid option)')
2035 
2036  if btype == 'MIX':
2037  row.prop(slot, "use_stencil", text="")
2038  row.prop(slot, "use_map_alpha", text="")
2039  if texop == 'blend_manual':
2040  row = box.row()
2041  row.label(text="Alpha:")
2042  row.prop(slot, "diffuse_color_factor", text="")
2043 
2044  if hasattr(slot.texture, 'image') and slot.texture.image:
2045  row = box.row()
2046  n = '(invalid option)'
2047  if slot.texture.extension in TextureUnit.tex_address_mode:
2048  n = TextureUnit.tex_address_mode[ slot.texture.extension ]
2049  row.prop(slot.texture, "extension", text=n)
2050  if slot.texture.extension == 'CLIP':
2051  row.prop(slot, "color", text="Border Color")
2052 
2053  row = box.row()
2054  if slot.texture_coords == 'UV':
2055  row.prop(slot, "texture_coords", text="", icon='GROUP_UVS')
2056  row.prop(slot, "uv_layer", text='Layer')
2057  elif slot.texture_coords == 'REFLECTION':
2058  row.prop(slot, "texture_coords", text="", icon='MOD_UVPROJECT')
2059  n = '(invalid option)'
2060  if slot.mapping in 'FLAT SPHERE'.split(): n = ''
2061  row.prop(slot, "mapping", text=n)
2062  else:
2063  row.prop(slot, "texture_coords", text="(invalid mapping option)")
2064 
2065  # Animation and offset options
2066  split = layout.row()
2067  box = split.box()
2068  box.prop(slot, "offset", text="XY=offset, Z=rotation")
2069  box = split.box()
2070  box.prop(slot, "scale", text="XY=scale (Z ignored)")
2071 
2072  box = layout.box()
2073  row = box.row()
2074  row.label(text='scrolling animation')
2075 
2076  # Can't use if its enabled by default row.prop(slot, "use_map_density", text="")
2077  row.prop(slot, "use_map_scatter", text="")
2078  row = box.row()
2079  row.prop(slot, "density_factor", text="X")
2080  row.prop(slot, "emission_factor", text="Y")
2081 
2082  box = layout.box()
2083  row = box.row()
2084  row.label(text='rotation animation')
2085  row.prop(slot, "emission_color_factor", text="")
2086  row.prop(slot, "use_from_dupli", text="")
2087 
2088  ## Image magick
2089  if hasattr(slot.texture, 'image') and slot.texture.image:
2090  img = slot.texture.image
2091  box = layout.box()
2092  row = box.row()
2093  row.prop( img, 'use_convert_format' )
2094  if img.use_convert_format:
2095  row.prop( img, 'convert_format' )
2096  if img.convert_format == 'jpg':
2097  box.prop( img, 'jpeg_quality' )
2098 
2099  row = box.row()
2100  row.prop( img, 'use_color_quantize', text='Reduce Colors' )
2101  if img.use_color_quantize:
2102  row.prop( img, 'use_color_quantize_dither', text='dither' )
2103  row.prop( img, 'color_quantize', text='colors' )
2104 
2105  row = box.row()
2106  row.prop( img, 'use_resize_half' )
2107  if not img.use_resize_half:
2108  row.prop( img, 'use_resize_absolute' )
2109  if img.use_resize_absolute:
2110  row = box.row()
2111  row.prop( img, 'resize_x' )
2112  row.prop( img, 'resize_y' )
2113 
2114 ## OgreMeshy
2115 
2116 class OgreMeshyPreviewOp(bpy.types.Operator):
2117  '''helper to open ogremeshy'''
2118  bl_idname = 'ogremeshy.preview'
2119  bl_label = "opens ogremeshy in a subprocess"
2120  bl_options = {'REGISTER'}
2121  preview = BoolProperty(name="preview", description="fast preview", default=True)
2122  groups = BoolProperty(name="preview merge groups", description="use merge groups", default=False)
2123  mesh = BoolProperty(name="update mesh", description="update mesh (disable for fast material preview", default=True)
2124 
2125  @classmethod
2126  def poll(cls, context):
2127  if context.active_object and context.active_object.type in ('MESH','EMPTY') and context.mode != 'EDIT_MESH':
2128  if context.active_object.type == 'EMPTY' and context.active_object.dupli_type != 'GROUP':
2129  return False
2130  else:
2131  return True
2132 
2133  def execute(self, context):
2134  Report.reset()
2135  Report.messages.append('running %s' %CONFIG['OGRE_MESHY'])
2136 
2137  if sys.platform.startswith('linux'):
2138  # If OgreMeshy ends with .exe, set the path for preview meshes to
2139  # the user's wine directory, otherwise to /tmp.
2140  if CONFIG['OGRE_MESHY'].endswith('.exe'):
2141  path = '%s/.wine/drive_c/tmp' % os.environ['HOME']
2142  else:
2143  path = '/tmp'
2144  elif sys.platform.startswith('darwin') or sys.platform.startswith('freebsd'):
2145  path = '/tmp'
2146  else:
2147  path = 'C:\\tmp'
2148 
2149  mat = None
2150  mgroup = merged = None
2151  umaterials = []
2152 
2153  if context.active_object.type == 'MESH':
2154  mat = context.active_object.active_material
2155  elif context.active_object.type == 'EMPTY': # assume group
2156  obs = []
2157  for e in context.selected_objects:
2158  if e.type != 'EMPTY' and e.dupli_group: continue
2159  grp = e.dupli_group
2160  subs = []
2161  for o in grp.objects:
2162  if o.type=='MESH': subs.append( o )
2163  if subs:
2164  m = merge_objects( subs, transform=e.matrix_world )
2165  obs.append( m )
2166  if obs:
2167  merged = merge_objects( obs )
2168  umaterials = dot_mesh( merged, path=path, force_name='preview' )
2169  for o in obs: context.scene.objects.unlink(o)
2170 
2171  if not self.mesh:
2172  for ob in context.selected_objects:
2173  if ob.type == 'MESH':
2174  for mat in ob.data.materials:
2175  if mat and mat not in umaterials: umaterials.append( mat )
2176 
2177  if not merged:
2178  mgroup = MeshMagick.get_merge_group( context.active_object )
2179  if not mgroup and self.groups:
2180  group = get_merge_group( context.active_object )
2181  if group:
2182  print('--------------- has merge group ---------------' )
2183  merged = merge_group( group )
2184  else:
2185  print('--------------- NO merge group ---------------' )
2186  elif len(context.selected_objects)>1 and context.selected_objects:
2187  merged = merge_objects( context.selected_objects )
2188 
2189  if mgroup:
2190  for ob in mgroup.objects:
2191  nmats = dot_mesh( ob, path=path )
2192  for m in nmats:
2193  if m not in umaterials: umaterials.append( m )
2194  MeshMagick.merge( mgroup, path=path, force_name='preview' )
2195  elif merged:
2196  umaterials = dot_mesh( merged, path=path, force_name='preview' )
2197  else:
2198  umaterials = dot_mesh( context.active_object, path=path, force_name='preview' )
2199 
2200  if mat or umaterials:
2201  #CONFIG['TOUCH_TEXTURES'] = True
2202  #CONFIG['PATH'] = path # TODO deprecate
2203  data = ''
2204  for umat in umaterials:
2205  data += generate_material( umat, path=path, copy_programs=True, touch_textures=True ) # copies shader programs to path
2206  f=open( os.path.join( path, 'preview.material' ), 'wb' )
2207  f.write( bytes(data,'utf-8') ); f.close()
2208 
2209  if merged: context.scene.objects.unlink( merged )
2210 
2211  if sys.platform.startswith('linux') or sys.platform.startswith('darwin') or sys.platform.startswith('freebsd'):
2212  if CONFIG['OGRE_MESHY'].endswith('.exe'):
2213  cmd = ['wine', CONFIG['OGRE_MESHY'], 'c:\\tmp\\preview.mesh' ]
2214  else:
2215  cmd = [CONFIG['OGRE_MESHY'], '/tmp/preview.mesh']
2216  print( cmd )
2217  #subprocess.call(cmd)
2218  subprocess.Popen(cmd)
2219  else:
2220  #subprocess.call([CONFIG_OGRE_MESHY, 'C:\\tmp\\preview.mesh'])
2221  subprocess.Popen( [CONFIG['OGRE_MESHY'], 'C:\\tmp\\preview.mesh'] )
2222 
2223  Report.show()
2224  return {'FINISHED'}
2225 
2226 ## Ogre Documentation to UI
2227 
2228 _OGRE_DOCS_ = []
2229 def ogredoc( cls ):
2230  tag = cls.__name__.split('_ogredoc_')[-1]
2231  cls.bl_label = tag.replace('_', ' ')
2232  _OGRE_DOCS_.append( cls )
2233  return cls
2234 
2235 class INFO_MT_ogre_helper(bpy.types.Menu):
2236  bl_label = '_overloaded_'
2237 
2238  def draw(self, context):
2239  layout = self.layout
2240  #row = self.layout.box().split(percentage=0.05)
2241  #col = row.column(align=False)
2242  #print(dir(col))
2243  #row.scale_x = 0.1
2244  #row.alignment = 'RIGHT'
2245 
2246  for line in self.mydoc.splitlines():
2247  if line.strip():
2248  for ww in wordwrap( line ): layout.label(text=ww)
2249  layout.separator()
2250 
2251 class INFO_MT_ogre_docs(bpy.types.Menu):
2252  bl_label = "Ogre Help"
2253 
2254  def draw(self, context):
2255  layout = self.layout
2256  for cls in _OGRE_DOCS_:
2257  layout.menu( cls.__name__ )
2258  layout.separator()
2259  layout.separator()
2260  layout.label(text='bug reports to: bhartsho@yahoo.com')
2261 
2262 class INFO_MT_ogre_shader_pass_attributes(bpy.types.Menu):
2263  bl_label = "Shader-Pass"
2264 
2265  def draw(self, context):
2266  layout = self.layout
2267  for cls in _OGRE_SHADER_REF_:
2268  layout.menu( cls.__name__ )
2269 
2270 class INFO_MT_ogre_shader_texture_attributes(bpy.types.Menu):
2271  bl_label = "Shader-Texture"
2272 
2273  def draw(self, context):
2274  layout = self.layout
2275  for cls in _OGRE_SHADER_REF_TEX_:
2276  layout.menu( cls.__name__ )
2277 
2278 @ogredoc
2279 class _ogredoc_Installing( INFO_MT_ogre_helper ):
2280  mydoc = _doc_installing_
2281 
2282 @ogredoc
2283 class _ogredoc_FAQ( INFO_MT_ogre_helper ):
2284  mydoc = _faq_
2285 
2286 @ogredoc
2287 class _ogredoc_Animation_System( INFO_MT_ogre_helper ):
2288  mydoc = '''
2289 Armature Animation System | OgreDotSkeleton
2290  Quick Start:
2291  1. select your armature and set a single keyframe on the object (loc,rot, or scl)
2292  . note, this step is just a hack for creating an action so you can then create an NLA track.
2293  . do not key in pose mode, unless you want to only export animation on the keyed bones.
2294  2. open the NLA, and convert the action into an NLA strip
2295  3. name the NLA strip(s)
2296  4. set the in and out frames for each strip ( the strip name becomes the Ogre track name )
2297 
2298  How it Works:
2299  The NLA strips can be blank, they are only used to define Ogre track names, and in and out frame ranges. You are free to animate the armature with constraints (no baking required), or you can used baked animation and motion capture. Blending that is driven by the NLA is also supported, if you don't want blending, put space between each strip.
2300 
2301  The OgreDotSkeleton (.skeleton) format supports multiple named tracks that can contain some or all of the bones of an armature. This feature can be exploited by a game engine for segmenting and animation blending. For example: lets say we want to animate the upper torso independently of the lower body while still using a single armature. This can be done by hijacking the NLA of the armature.
2302 
2303  Advanced NLA Hijacking (selected-bones-animation):
2304  . define an action and keyframe only the bones you want to 'group', ie. key all the upper torso bones
2305  . import the action into the NLA
2306  . name the strip (this becomes the track name in Ogre)
2307  . adjust the start and end frames of each strip
2308  ( you may use multiple NLA tracks, multiple strips per-track is ok, and strips may overlap in time )
2309 
2310 '''
2311 
2312 @ogredoc
2313 class _ogredoc_Physics( INFO_MT_ogre_helper ):
2314  mydoc = '''
2315 Ogre Dot Scene + BGE Physics
2316  extended format including external collision mesh, and BGE physics settings
2317 <node name="...">
2318  <entity name="..." meshFile="..." collisionFile="..." collisionPrim="..." [and all BGE physics attributes] />
2319 </node>
2320 
2321 collisionFile : sets path to .mesh that is used for collision (ignored if collisionPrim is set)
2322 collisionPrim : sets optimal collision type [ cube, sphere, capsule, cylinder ]
2323 *these collisions are static meshes, animated deforming meshes should give the user a warning that they have chosen a static mesh collision type with an object that has an armature
2324 
2325 Blender Collision Setup:
2326  1. If a mesh object has a child mesh with a name starting with 'collision', then the child becomes the collision mesh for the parent mesh.
2327 
2328  2. If 'Collision Bounds' game option is checked, the bounds type [box, sphere, etc] is used. This will override above rule.
2329 
2330  3. Instances (and instances generated by optimal array modifier) will share the same collision type of the first instance, you DO NOT need to set the collision type for each instance.
2331 
2332 '''
2333 
2334 @ogredoc
2335 class _ogredoc_Bugs( INFO_MT_ogre_helper ):
2336  mydoc = '''
2337 Known Issues:
2338  . shape animation breaks when using modifiers that change the vertex count
2339  (Any modifier that changes the vertex count is bad with shape anim or armature anim)
2340  . never rename the nodes created by enabling Ogre-Material-Layers
2341  . never rename collision proxy meshes created by the Collision Panel
2342  . lighting in Tundra is not excatly the same as in Blender
2343 Tundra Streaming:
2344  . only supports streaming transform of up to 10 objects selected objects
2345  . the 3D view must be shown at the time you open Tundra
2346  . the same 3D view must be visible to stream data to Tundra
2347  . only position and scale are updated, a bug on the Tundra side prevents rotation update
2348  . animation playback is broken if you rename your NLA strips after opening Tundra
2349 '''
2350 
2351 # Ogre v1.7 Doc
2352 
2353 def _mesh_entity_helper( doc, ob, o ):
2354  ## extended format - BGE Physics ##
2355  o.setAttribute('mass', str(ob.game.mass))
2356  o.setAttribute('mass_radius', str(ob.game.radius))
2357  o.setAttribute('physics_type', ob.game.physics_type)
2358  o.setAttribute('actor', str(ob.game.use_actor))
2359  o.setAttribute('ghost', str(ob.game.use_ghost))
2360  o.setAttribute('velocity_min', str(ob.game.velocity_min))
2361  o.setAttribute('velocity_max', str(ob.game.velocity_max))
2362  o.setAttribute('lock_trans_x', str(ob.game.lock_location_x))
2363  o.setAttribute('lock_trans_y', str(ob.game.lock_location_y))
2364  o.setAttribute('lock_trans_z', str(ob.game.lock_location_z))
2365  o.setAttribute('lock_rot_x', str(ob.game.lock_rotation_x))
2366  o.setAttribute('lock_rot_y', str(ob.game.lock_rotation_y))
2367  o.setAttribute('lock_rot_z', str(ob.game.lock_rotation_z))
2368  o.setAttribute('anisotropic_friction', str(ob.game.use_anisotropic_friction))
2369  x,y,z = ob.game.friction_coefficients
2370  o.setAttribute('friction_x', str(x))
2371  o.setAttribute('friction_y', str(y))
2372  o.setAttribute('friction_z', str(z))
2373  o.setAttribute('damping_trans', str(ob.game.damping))
2374  o.setAttribute('damping_rot', str(ob.game.rotation_damping))
2375  o.setAttribute('inertia_tensor', str(ob.game.form_factor))
2376 
2377  mesh = ob.data
2378  # custom user props
2379  for prop in mesh.items():
2380  propname, propvalue = prop
2381  if not propname.startswith('_'):
2382  user = doc.createElement('user_data')
2383  o.appendChild( user )
2384  user.setAttribute( 'name', propname )
2385  user.setAttribute( 'value', str(propvalue) )
2386  user.setAttribute( 'type', type(propvalue).__name__ )
2387 
2388 #class _type(bpy.types.IDPropertyGroup):
2389 # name = StringProperty(name="jpeg format", description="", maxlen=64, default="")
2390 
2391 def get_lights_by_type( T ):
2392  r = []
2393  for ob in bpy.context.scene.objects:
2394  if ob.type=='LAMP':
2395  if ob.data.type==T: r.append( ob )
2396  return r
2397 
2398 class _TXML_(object):
2399  '''
2400  <component type="EC_Script" sync="1" name="myscript">
2401  <attribute value="" name="Script ref"/>
2402  <attribute value="false" name="Run on load"/>
2403  <attribute value="0" name="Run mode"/>
2404  <attribute value="" name="Script application name"/>
2405  <attribute value="" name="Script class name"/>
2406  </component>
2407  '''
2408 
2409  def create_tundra_document( self, context ):
2410  # todo: Make a way in the gui to give prefix for the refs
2411  # This can be very useful if you want to give deployment URL
2412  # eg. "http://www.myassets.com/myscene/". By default this needs
2413  # to be an empty string, it will operate best for local preview
2414  # and importing the scene content to existing scenes with relative refs.
2415  proto = ''
2416 
2417  doc = RDocument()
2418  scn = doc.createElement('scene')
2419  doc.appendChild( scn )
2420 
2421  # EC_Script
2422  if 0: # todo: tundra bug (what does this mean?)
2423  e = doc.createElement( 'entity' )
2424  doc.documentElement.appendChild( e )
2425  e.setAttribute('id', len(doc.documentElement.childNodes)+1 )
2426 
2427  c = doc.createElement( 'component' ); e.appendChild( c )
2428  c.setAttribute( 'type', 'EC_Script' )
2429  c.setAttribute( 'sync', '1' )
2430  c.setAttribute( 'name', 'myscript' )
2431 
2432  a = doc.createElement('attribute'); c.appendChild( a )
2433  a.setAttribute('name', 'Script ref')
2434  #a.setAttribute('value', "%s%s"%(proto,TUNDRA_GEN_SCRIPT_PATH) )
2435 
2436  a = doc.createElement('attribute'); c.appendChild( a )
2437  a.setAttribute('name', 'Run on load')
2438  a.setAttribute('value', 'true' )
2439 
2440  a = doc.createElement('attribute'); c.appendChild( a )
2441  a.setAttribute('name', 'Run mode')
2442  a.setAttribute('value', '0' )
2443 
2444  a = doc.createElement('attribute'); c.appendChild( a )
2445  a.setAttribute('name', 'Script application name')
2446  a.setAttribute('value', 'blender2ogre' )
2447 
2448  # Check lighting settings
2449  sun = hemi = None
2450  if get_lights_by_type('SUN'):
2451  sun = get_lights_by_type('SUN')[0]
2452  if get_lights_by_type('HEMI'):
2453  hemi = get_lights_by_type('HEMI')[0]
2454 
2455  # Environment
2456  if bpy.context.scene.world.mist_settings.use_mist or sun or hemi:
2457  # Entity for environment components
2458  e = doc.createElement( 'entity' )
2459  doc.documentElement.appendChild( e )
2460  e.setAttribute('id', len(doc.documentElement.childNodes)+1 )
2461 
2462  # EC_Fog
2463  c = doc.createElement( 'component' ); e.appendChild( c )
2464  c.setAttribute( 'type', 'EC_Fog' )
2465  c.setAttribute( 'sync', '1' )
2466  c.setAttribute( 'name', 'Fog' )
2467 
2468  a = doc.createElement('attribute'); c.appendChild( a )
2469  a.setAttribute('name', 'Color')
2470  if bpy.context.scene.world.mist_settings.use_mist:
2471  A = bpy.context.scene.world.mist_settings.intensity
2472  R,G,B = bpy.context.scene.world.horizon_color
2473  a.setAttribute('value', '%s %s %s %s'%(R,G,B,A))
2474  else:
2475  a.setAttribute('value', '0.4 0.4 0.4 1.0')
2476 
2477  if bpy.context.scene.world.mist_settings.use_mist:
2478  mist = bpy.context.scene.world.mist_settings
2479 
2480  a = doc.createElement('attribute'); c.appendChild( a )
2481  a.setAttribute('name', 'Start distance')
2482  a.setAttribute('value', mist.start)
2483 
2484  a = doc.createElement('attribute'); c.appendChild( a )
2485  a.setAttribute('name', 'End distance')
2486  a.setAttribute('value', mist.start+mist.depth)
2487 
2488  a = doc.createElement('attribute'); c.appendChild( a )
2489  a.setAttribute('name', 'Exponential density')
2490  a.setAttribute('value', 0.001)
2491 
2492  # EC_EnvironmentLight
2493  c = doc.createElement( 'component' ); e.appendChild( c )
2494  c.setAttribute( 'type', 'EC_EnvironmentLight' )
2495  c.setAttribute( 'sync', '1' )
2496  c.setAttribute( 'name', 'Environment Light' )
2497 
2498  a = doc.createElement('attribute'); c.appendChild( a )
2499  a.setAttribute('name', 'Sunlight color')
2500  if sun:
2501  R,G,B = sun.data.color
2502  a.setAttribute('value', '%s %s %s 1' %(R,G,B))
2503  else:
2504  a.setAttribute('value', '0 0 0 1')
2505 
2506  a = doc.createElement('attribute'); c.appendChild( a )
2507  a.setAttribute('name', 'Brightness') # brightness of sunlight
2508  if sun:
2509  a.setAttribute('value', sun.data.energy*10) # 10=magic
2510  else:
2511  a.setAttribute('value', '0')
2512 
2513  a = doc.createElement('attribute'); c.appendChild( a )
2514  a.setAttribute('name', 'Ambient light color')
2515  if hemi:
2516  R,G,B = hemi.data.color * hemi.data.energy * 3.0
2517  if R>1.0: R=1.0
2518  if G>1.0: G=1.0
2519  if B>1.0: B=1.0
2520  a.setAttribute('value', '%s %s %s 1' %(R,G,B))
2521  else:
2522  a.setAttribute('value', '0 0 0 1')
2523 
2524  a = doc.createElement('attribute'); c.appendChild( a )
2525  a.setAttribute('name', 'Sunlight direction vector')
2526  a.setAttribute('value', '-0.25 -1.0 -0.25') # TODO, get the sun rotation from blender
2527 
2528  a = doc.createElement('attribute'); c.appendChild( a )
2529  a.setAttribute('name', 'Sunlight cast shadows')
2530  a.setAttribute('value', 'true')
2531 
2532  # EC_SkyX
2533  if context.scene.world.ogre_skyX:
2534  c = doc.createElement( 'component' ); e.appendChild( c )
2535  c.setAttribute( 'type', 'EC_SkyX' )
2536  c.setAttribute( 'sync', '1' )
2537  c.setAttribute( 'name', 'SkyX' )
2538 
2539  a = doc.createElement('attribute'); a.setAttribute('name', 'Weather (volumetric clouds only)')
2540  den = (
2541  context.scene.world.ogre_skyX_cloud_density_x,
2542  context.scene.world.ogre_skyX_cloud_density_y
2543  )
2544  a.setAttribute('value', '%s %s' %den)
2545  c.appendChild( a )
2546 
2547  config = (
2548  ('time', 'Time multiplier'),
2549  ('volumetric_clouds','Volumetric clouds'),
2550  ('wind','Wind direction'),
2551  )
2552  for bname, aname in config:
2553  a = doc.createElement('attribute')
2554  a.setAttribute('name', aname)
2555  s = str( getattr(context.scene.world, 'ogre_skyX_'+bname) )
2556  a.setAttribute('value', s.lower())
2557  c.appendChild( a )
2558 
2559  return doc
2560 
2561  # Creates new Tundra entity
2562  def tundra_entity( self, doc, ob, path='/tmp', collision_proxies=[], parent=None, matrix=None,visible=True ):
2563  assert not ob.subcollision
2564 
2565  # Tundra TRANSFORM
2566  if not matrix:
2567  matrix = ob.matrix_world.copy()
2568 
2569  # todo: Make a way in the gui to give prefix for the refs
2570  # This can be very useful if you want to give deployment URL
2571  # eg. "http://www.myassets.com/myscene/". By default this needs
2572  # to be an empty string, it will operate best for local preview
2573  # and importing the scene content to existing scenes with relative refs.
2574  proto = ''
2575 
2576  # Entity
2577  entityid = uid(ob)
2578  objectname = clean_object_name(ob.name)
2579  print(" Creating Tundra Enitity with ID", entityid)
2580 
2581  e = doc.createElement( 'entity' )
2582  doc.documentElement.appendChild( e )
2583  e.setAttribute('id', entityid)
2584 
2585  # EC_Name
2586  print (" - EC_Name with", objectname)
2587 
2588  c = doc.createElement('component'); e.appendChild( c )
2589  c.setAttribute('type', "EC_Name")
2590  c.setAttribute('sync', '1')
2591  a = doc.createElement('attribute'); c.appendChild(a)
2592  a.setAttribute('name', "name" )
2593  a.setAttribute('value', objectname )
2594  a = doc.createElement('attribute'); c.appendChild(a)
2595  a.setAttribute('name', "description" )
2596  a.setAttribute('value', "" )
2597 
2598  # EC_Placeable
2599  print (" - EC_Placeable ")
2600 
2601  c = doc.createElement('component'); e.appendChild( c )
2602  c.setAttribute('type', "EC_Placeable")
2603  c.setAttribute('sync', '1')
2604  a = doc.createElement('attribute'); c.appendChild(a)
2605  a.setAttribute('name', "Transform" )
2606  x,y,z = swap(matrix.to_translation())
2607  loc = '%6f,%6f,%6f' %(x,y,z)
2608  x,y,z = swap(matrix.to_euler())
2609  x = math.degrees( x ); y = math.degrees( y ); z = math.degrees( z )
2610  if ob.type == 'CAMERA':
2611  x -= 90
2612  elif ob.type == 'LAMP':
2613  x += 90
2614  rot = '%6f,%6f,%6f' %(x,y,z)
2615  x,y,z = swap(matrix.to_scale())
2616  scl = '%6f,%6f,%6f' %(abs(x),abs(y),abs(z)) # Tundra2 clamps any negative to zero
2617  a.setAttribute('value', "%s,%s,%s" %(loc,rot,scl) )
2618 
2619  a = doc.createElement('attribute'); c.appendChild(a)
2620  a.setAttribute('name', "Show bounding box" )
2621  a.setAttribute('value', "false" )
2622  # Don't mark bounding boxes to show in Tundra!
2623  #if ob.show_bounds or ob.type != 'MESH':
2624  # a.setAttribute('value', "true" )
2625  #else:
2626  # a.setAttribute('value', "false" )
2627 
2628  a = doc.createElement('attribute'); c.appendChild(a)
2629  a.setAttribute('name', "Visible" )
2630  if visible:
2631  a.setAttribute('value', 'true') # overrides children's setting - not good!
2632  else:
2633  a.setAttribute('value', 'false')
2634 
2635  a = doc.createElement('attribute'); c.appendChild(a)
2636  a.setAttribute('name', "Selection layer" )
2637  a.setAttribute('value', 1)
2638 
2639  # Tundra parenting to EC_Placeable.
2640  # todo: Verify this inserts correct ent name or id here.
2641  # <attribute value="" name="Parent entity ref"/>
2642  # <attribute value="" name="Parent bone name"/>
2643  if parent:
2644  a = doc.createElement('attribute'); c.appendChild(a)
2645  a.setAttribute('name', "Parent entity ref" )
2646  a.setAttribute('value', parent)
2647 
2648  if ob.type != 'MESH':
2649  c = doc.createElement('component'); e.appendChild( c )
2650  c.setAttribute('type', 'EC_Name')
2651  c.setAttribute('sync', '1')
2652  a = doc.createElement('attribute'); c.appendChild(a)
2653  a.setAttribute('name', "name" )
2654  a.setAttribute('value', objectname)
2655 
2656  # EC_Sound: Supports wav and ogg
2657  if ob.type == 'SPEAKER':
2658  print (" - EC_Sound")
2659  c = doc.createElement('component'); e.appendChild( c )
2660  c.setAttribute('type', 'EC_Sound')
2661  c.setAttribute('sync', '1')
2662 
2663  if ob.data.sound:
2664  abspath = bpy.path.abspath( ob.data.sound.filepath )
2665  soundpath, soundfile = os.path.split( abspath )
2666  soundref = "%s%s" % (proto, soundfile)
2667  print (" Sounds ref:", soundref)
2668  a = doc.createElement('attribute'); c.appendChild(a)
2669  a.setAttribute('name', 'Sound ref' )
2670  a.setAttribute('value', soundref)
2671  if not os.path.isfile( os.path.join(path,soundfile) ):
2672  open( os.path.join(path,soundfile), 'wb' ).write( open(abspath,'rb').read() )
2673 
2674  a = doc.createElement('attribute'); c.appendChild(a)
2675  a.setAttribute('name', 'Sound radius inner' )
2676  a.setAttribute('value', ob.data.cone_angle_inner)
2677 
2678  a = doc.createElement('attribute'); c.appendChild(a)
2679  a.setAttribute('name', 'Sound radius outer' )
2680  a.setAttribute('value', ob.data.cone_angle_outer)
2681 
2682  a = doc.createElement('attribute'); c.appendChild(a)
2683  a.setAttribute('name', 'Sound gain' )
2684  a.setAttribute('value', ob.data.volume)
2685 
2686  a = doc.createElement('attribute'); c.appendChild(a)
2687  a.setAttribute('name', 'Play on load' )
2688  if ob.data.play_on_load:
2689  a.setAttribute('value', 'true')
2690  else:
2691  a.setAttribute('value', 'false')
2692 
2693  a = doc.createElement('attribute'); c.appendChild(a)
2694  a.setAttribute('name', 'Loop sound' )
2695  if ob.data.loop:
2696  a.setAttribute('value', 'true')
2697  else:
2698  a.setAttribute('value', 'false')
2699 
2700  a = doc.createElement('attribute'); c.appendChild(a)
2701  a.setAttribute('name', 'Spatial' )
2702  if ob.data.use_spatial:
2703  a.setAttribute('value', 'true')
2704  else:
2705  a.setAttribute('value', 'false')
2706 
2707  # EC_Camera
2708  ''' todo: This is really not very helpful. Apps define
2709  camera logic in Tundra. By default you will have
2710  a freecamera to move around the scene etc. This created
2711  camera wont be activated except if a script does so.
2712  Best leave camera (creation) logic for the inworld apps.
2713  At least remove the default "export cameras" for txml. '''
2714  if ob.type == 'CAMERA':
2715  print (" - EC_Camera")
2716  c = doc.createElement('component'); e.appendChild( c )
2717  c.setAttribute('type', 'EC_Camera')
2718  c.setAttribute('sync', '1')
2719  a = doc.createElement('attribute'); c.appendChild(a)
2720  a.setAttribute('name', "Up vector" )
2721  a.setAttribute('value', '0.0 1.0 0.0')
2722  a = doc.createElement('attribute'); c.appendChild(a)
2723  a.setAttribute('name', "Near plane" )
2724  a.setAttribute('value', '0.01')
2725  a = doc.createElement('attribute'); c.appendChild(a)
2726  a.setAttribute('name', "Far plane" )
2727  a.setAttribute('value', '2000')
2728  a = doc.createElement('attribute'); c.appendChild(a)
2729  a.setAttribute('name', "Vertical FOV" )
2730  a.setAttribute('value', '45')
2731  a = doc.createElement('attribute'); c.appendChild(a)
2732  a.setAttribute('name', "Aspect ratio" )
2733  a.setAttribute('value', '')
2734 
2735  NTF = None
2736 
2737  # EC_Rigidbody
2738  # Any object can have physics, although it needs
2739  # EC_Placeable to have position etc.
2740  if ob.physics_mode != 'NONE' or ob.collision_mode != 'NONE':
2741  TundraTypes = {
2742  'BOX' : 0,
2743  'SPHERE' : 1,
2744  'CYLINDER' : 2,
2745  'CONE' : 0, # Missing in Tundra
2746  'CAPSULE' : 3,
2747  'TRIANGLE_MESH' : 4,
2748  #'HEIGHT_FIELD': 5, # Missing in Blender
2749  'CONVEX_HULL' : 6
2750  }
2751 
2752  com = doc.createElement('component'); e.appendChild( com )
2753  com.setAttribute('type', 'EC_RigidBody')
2754  com.setAttribute('sync', '1')
2755 
2756  # Mass
2757  # * Does not affect static collision types (TriMesh and ConvexHull)
2758  # * You can have working collisions with mass 0
2759  a = doc.createElement('attribute'); com.appendChild( a )
2760  a.setAttribute('name', 'Mass')
2761  if ob.physics_mode == 'RIGID_BODY':
2762  a.setAttribute('value', ob.game.mass)
2763  else:
2764  a.setAttribute('value', '0.0')
2765 
2766  SHAPE = a = doc.createElement('attribute'); com.appendChild( a )
2767  a.setAttribute('name', 'Shape type')
2768  a.setAttribute('value', TundraTypes[ ob.game.collision_bounds_type ] )
2769 
2770  print (" - EC_RigidBody with shape type", TundraTypes[ob.game.collision_bounds_type])
2771 
2772  M = ob.game.collision_margin
2773  a = doc.createElement('attribute'); com.appendChild( a )
2774  a.setAttribute('name', 'Size')
2775  if ob.game.collision_bounds_type in 'TRIANGLE_MESH CONVEX_HULL'.split():
2776  a.setAttribute('value', '%s %s %s' %(1.0+M, 1.0+M, 1.0+M) )
2777  else:
2778  #x,y,z = swap(ob.matrix_world.to_scale())
2779  x,y,z = swap(ob.dimensions)
2780  a.setAttribute('value', '%s %s %s' %(abs(x)+M,abs(y)+M,abs(z)+M) )
2781 
2782  a = doc.createElement('attribute'); com.appendChild( a )
2783  a.setAttribute('name', 'Collision mesh ref')
2784  #if ob.game.use_collision_compound:
2785  if ob.collision_mode == 'DECIMATED':
2786  proxy = None
2787  for child in ob.children:
2788  if child.subcollision and child.name.startswith('DECIMATED'):
2789  proxy = child; break
2790  if proxy:
2791  collisionref = "%s_collision_%s.mesh" % (proto, proxy.data.name)
2792  a.setAttribute('value', collisionref)
2793  if proxy not in collision_proxies:
2794  collision_proxies.append( proxy )
2795  else:
2796  print('[WARNINIG]: Collision proxy mesh not found' )
2797  assert 0
2798  elif ob.collision_mode == 'TERRAIN':
2799  NTF = save_terrain_as_NTF( path, ob )
2800  SHAPE.setAttribute( 'value', '5' ) # HEIGHT_FIELD
2801  elif ob.type == 'MESH':
2802  # todo: Remove this. There is no need to set mesh collision ref
2803  # if TriMesh or ConvexHull is used, it will be auto picked from EC_Mesh
2804  # in the same Entity.
2805  collisionref = "%s%s.mesh" % (proto, clean_object_name(ob.data.name))
2806  a.setAttribute('value', collisionref)
2807 
2808  a = doc.createElement('attribute'); com.appendChild( a )
2809  a.setAttribute('name', 'Friction')
2810  #avg = sum( ob.game.friction_coefficients ) / 3.0
2811  a.setAttribute('value', ob.physics_friction)
2812 
2813  a = doc.createElement('attribute'); com.appendChild( a )
2814  a.setAttribute('name', 'Restitution')
2815  a.setAttribute('value', ob.physics_bounce)
2816 
2817  a = doc.createElement('attribute'); com.appendChild( a )
2818  a.setAttribute('name', 'Linear damping')
2819  a.setAttribute('value', ob.game.damping)
2820 
2821  a = doc.createElement('attribute'); com.appendChild( a )
2822  a.setAttribute('name', 'Angular damping')
2823  a.setAttribute('value', ob.game.rotation_damping)
2824 
2825  a = doc.createElement('attribute'); com.appendChild( a )
2826  a.setAttribute('name', 'Linear factor')
2827  a.setAttribute('value', '1.0 1.0 1.0')
2828 
2829  a = doc.createElement('attribute'); com.appendChild( a )
2830  a.setAttribute('name', 'Angular factor')
2831  a.setAttribute('value', '1.0 1.0 1.0')
2832 
2833  a = doc.createElement('attribute'); com.appendChild( a )
2834  a.setAttribute('name', 'Kinematic')
2835  a.setAttribute('value', 'false' )
2836 
2837  # todo: Find out what Phantom actually means and if this
2838  # needs to be set for NONE collision rigids. I don't actually
2839  # see any reason to make EC_RigidBody if collision is NONE
2840  a = doc.createElement('attribute'); com.appendChild( a )
2841  a.setAttribute('name', 'Phantom')
2842  if ob.collision_mode == 'NONE':
2843  a.setAttribute('value', 'true' )
2844  else:
2845  a.setAttribute('value', 'false' )
2846 
2847  a = doc.createElement('attribute'); com.appendChild( a )
2848  a.setAttribute('name', 'Draw Debug')
2849  a.setAttribute('value', 'false' )
2850 
2851  # Never mark rigids to have draw debug, it can
2852  # be toggled in tundra for visual debugging.
2853  #if ob.collision_mode == 'NONE':
2854  # a.setAttribute('value', 'false' )
2855  #else:
2856  # a.setAttribute('value', 'true' )
2857 
2858  a = doc.createElement('attribute'); com.appendChild( a )
2859  a.setAttribute('name', 'Linear velocity')
2860  a.setAttribute('value', '0.0 0.0 0.0')
2861 
2862  a = doc.createElement('attribute'); com.appendChild( a )
2863  a.setAttribute('name', 'Angular velocity')
2864  a.setAttribute('value', '0.0 0.0 0.0')
2865 
2866  a = doc.createElement('attribute'); com.appendChild( a )
2867  a.setAttribute('name', 'Collision Layer')
2868  a.setAttribute('value', -1)
2869 
2870  a = doc.createElement('attribute'); com.appendChild( a )
2871  a.setAttribute('name', 'Collision Mask')
2872  a.setAttribute('value', -1)
2873 
2874  # EC_Terrain
2875  if NTF:
2876  xp = NTF['xpatches']
2877  yp = NTF['ypatches']
2878  depth = NTF['depth']
2879 
2880  print (" - EC_Terrain")
2881  com = doc.createElement('component'); e.appendChild( com )
2882  com.setAttribute('type', 'EC_Terrain')
2883  com.setAttribute('sync', '1')
2884 
2885  a = doc.createElement('attribute'); com.appendChild( a )
2886  a.setAttribute('name', 'Transform')
2887  x,y,z = ob.dimensions
2888  sx,sy,sz = ob.scale
2889  x *= 1.0/sx
2890  y *= 1.0/sy
2891  z *= 1.0/sz
2892  #trans = '%s,%s,%s,' %(-xp/4, -z/2, -yp/4)
2893  trans = '%s,%s,%s,' %(-xp/4, -depth, -yp/4)
2894  # scaling in Tundra happens after translation
2895  nx = x/(xp*16)
2896  ny = y/(yp*16)
2897  trans += '0,0,0,%s,%s,%s' %(nx,depth, ny)
2898  a.setAttribute('value', trans )
2899 
2900  a = doc.createElement('attribute'); com.appendChild( a )
2901  a.setAttribute('name', 'Grid Width')
2902  a.setAttribute('value', xp)
2903 
2904  a = doc.createElement('attribute'); com.appendChild( a )
2905  a.setAttribute('name', 'Grid Height')
2906  a.setAttribute('value', yp)
2907 
2908  a = doc.createElement('attribute'); com.appendChild( a )
2909  a.setAttribute('name', 'Tex. U scale')
2910  a.setAttribute('value', 1.0)
2911 
2912  a = doc.createElement('attribute'); com.appendChild( a )
2913  a.setAttribute('name', 'Tex. V scale')
2914  a.setAttribute('value', 1.0)
2915 
2916  a = doc.createElement('attribute'); com.appendChild( a )
2917  a.setAttribute('name', 'Material')
2918  a.setAttribute('value', '')
2919 
2920  for i in range(4):
2921  a = doc.createElement('attribute'); com.appendChild( a )
2922  a.setAttribute('name', 'Texture %s' %i)
2923  a.setAttribute('value', '')
2924 
2925  # todo: Check that NTF['name'] is the actual valid asset ref
2926  # and not the disk path.
2927  heightmapref = "%s%s" % (proto, NTF['name'])
2928  print (" Heightmap ref:", heightmapref)
2929  a = doc.createElement('attribute'); com.appendChild( a )
2930  a.setAttribute('name', 'Heightmap')
2931  a.setAttribute('value', heightmapref )
2932 
2933  # Enitity XML generation done, return the element.
2934  return e
2935 
2936  # EC_Mesh
2937  def tundra_mesh( self, e, ob, url, exported_meshes ):
2938  # todo: Make a way in the gui to give prefix for the refs
2939  # This can be very useful if you want to give deployment URL
2940  # eg. "http://www.myassets.com/myscene/". By default this needs
2941  # to be an empty string, it will operate best for local preview
2942  # and importing the scene content to existing scenes with relative refs.
2943  proto = ''
2944 
2945  objectname = clean_object_name(ob.data.name)
2946  meshname = "%s.mesh" % objectname
2947  meshref = "%s%s.mesh" % (proto, objectname)
2948 
2949  print (" - EC_Mesh")
2950  print (" - Mesh ref:", meshref)
2951 
2952  if self.EX_MESH:
2953  murl = os.path.join( os.path.split(url)[0], meshname )
2954  exists = os.path.isfile( murl )
2955  if not exists or (exists and self.EX_MESH_OVERWRITE):
2956  if meshname not in exported_meshes:
2957  exported_meshes.append( meshname )
2958  self.dot_mesh( ob, os.path.split(url)[0] )
2959 
2960  doc = e.document
2961 
2962  if ob.find_armature():
2963  print (" - EC_AnimationController")
2964  c = doc.createElement('component'); e.appendChild( c )
2965  c.setAttribute('type', "EC_AnimationController")
2966  c.setAttribute('sync', '1')
2967 
2968  c = doc.createElement('component'); e.appendChild( c )
2969  c.setAttribute('type', "EC_Mesh")
2970  c.setAttribute('sync', '1')
2971 
2972  a = doc.createElement('attribute'); c.appendChild(a)
2973  a.setAttribute('name', "Mesh ref" )
2974  a.setAttribute('value', meshref)
2975 
2976  a = doc.createElement('attribute'); c.appendChild(a)
2977  a.setAttribute('name', "Mesh materials" )
2978 
2979  # Query object its materials and make a proper material ref string of it.
2980  # note: We assume blindly here that the 'submesh' indexes are correct in the material list.
2981  mymaterials = ob.data.materials
2982  if mymaterials is not None and len(mymaterials) > 0:
2983  mymatstring = '' # generate ; separated material list
2984  for mymat in mymaterials:
2985  if mymat is None:
2986  continue
2987 
2988  mymatstring += proto + material_name(mymat, True) + '.material;'
2989  mymatstring = mymatstring[:-1] # strip ending ;
2990  a.setAttribute('value', mymatstring )
2991  else:
2992  # default to nothing to avoid error prints in .txml import
2993  a.setAttribute('value', "" )
2994 
2995  if ob.find_armature():
2996  skeletonref = "%s%s.skeleton" % (proto, clean_object_name(ob.data.name))
2997  print (" Skeleton ref:", skeletonref)
2998  a = doc.createElement('attribute'); c.appendChild(a)
2999  a.setAttribute('name', "Skeleton ref" )
3000  a.setAttribute('value', skeletonref)
3001 
3002  a = doc.createElement('attribute'); c.appendChild(a)
3003  a.setAttribute('name', "Draw distance" )
3004  if ob.use_draw_distance:
3005  a.setAttribute('value', ob.draw_distance )
3006  else:
3007  a.setAttribute('value', "0" )
3008 
3009  a = doc.createElement('attribute'); c.appendChild(a)
3010  a.setAttribute('name', 'Cast shadows' )
3011  if ob.cast_shadows:
3012  a.setAttribute('value', 'true' )
3013  else:
3014  a.setAttribute('value', 'false' )
3015 
3016  # EC_Light
3017  def tundra_light( self, e, ob ):
3018  '''
3019  <component type="EC_Light" sync="1">
3020  <attribute value="1" name="light type"/>
3021  <attribute value="1 1 1 1" name="diffuse color"/>
3022  <attribute value="1 1 1 1" name="specular color"/>
3023  <attribute value="true" name="cast shadows"/>
3024  <attribute value="29.9999828" name="light range"/>
3025  <attribute value="1" name="brightness"/>
3026  <attribute value="0" name="constant atten"/>
3027  <attribute value="1" name="linear atten"/>
3028  <attribute value="0" name="quadratic atten"/>
3029  <attribute value="30" name="light inner angle"/>
3030  <attribute value="40" name="light outer angle"/>
3031  </component>
3032  '''
3033 
3034  if ob.data.type not in 'POINT SPOT'.split():
3035  return
3036 
3037  doc = e.document
3038 
3039  c = doc.createElement('component'); e.appendChild( c )
3040  c.setAttribute('type', "EC_Light")
3041  c.setAttribute('sync', '1')
3042 
3043  a = doc.createElement('attribute'); c.appendChild(a)
3044  a.setAttribute('name', 'light type' )
3045  if ob.data.type=='POINT':
3046  a.setAttribute('value', '0' )
3047  elif ob.data.type=='SPOT':
3048  a.setAttribute('value', '1' )
3049  #2 = directional light. blender has no directional light?
3050 
3051  R,G,B = ob.data.color
3052  a = doc.createElement('attribute'); c.appendChild(a)
3053  a.setAttribute('name', 'diffuse color' )
3054  if ob.data.use_diffuse:
3055  a.setAttribute('value', '%s %s %s 1' %(R,G,B) )
3056  else:
3057  a.setAttribute('value', '0 0 0 1' )
3058 
3059  a = doc.createElement('attribute'); c.appendChild(a)
3060  a.setAttribute('name', 'specular color' )
3061  if ob.data.use_specular:
3062  a.setAttribute('value', '%s %s %s 1' %(R,G,B) )
3063  else:
3064  a.setAttribute('value', '0 0 0 1' )
3065 
3066  a = doc.createElement('attribute'); c.appendChild(a)
3067  a.setAttribute('name', 'cast shadows' )
3068  if ob.data.type=='HEMI':
3069  a.setAttribute('value', 'false' ) # HEMI no .shadow_method
3070  elif ob.data.shadow_method != 'NOSHADOW':
3071  a.setAttribute('value', 'true' )
3072  else:
3073  a.setAttribute('value', 'false' )
3074 
3075  a = doc.createElement('attribute'); c.appendChild(a)
3076  a.setAttribute('name', 'light range' )
3077  a.setAttribute('value', ob.data.distance*2 )
3078 
3079  a = doc.createElement('attribute'); c.appendChild(a)
3080  a.setAttribute('name', 'brightness' )
3081  a.setAttribute('value', ob.data.energy )
3082 
3083  a = doc.createElement('attribute'); c.appendChild(a)
3084  a.setAttribute('name', 'constant atten' )
3085  a.setAttribute('value', '0' )
3086 
3087  a = doc.createElement('attribute'); c.appendChild(a)
3088  a.setAttribute('name', 'linear atten' )
3089  energy = ob.data.energy
3090  if energy <= 0.0:
3091  energy = 0.001
3092  a.setAttribute('value', (1.0/energy)*0.25 )
3093 
3094  a = doc.createElement('attribute'); c.appendChild(a)
3095  a.setAttribute('name', 'quadratic atten' )
3096  a.setAttribute('value', '0.0' )
3097 
3098  if ob.data.type=='SPOT':
3099  outer = math.degrees(ob.data.spot_size) / 2.0
3100  inner = outer * (1.0-ob.data.spot_blend)
3101 
3102  a = doc.createElement('attribute'); c.appendChild(a)
3103  a.setAttribute('name', 'light inner angle' )
3104  a.setAttribute('value', '%s'%inner )
3105 
3106  a = doc.createElement('attribute'); c.appendChild(a)
3107  a.setAttribute('name', 'light outer angle' )
3108  a.setAttribute('value', '%s' %outer )
3109 
3110 ## UI export panel
3111 
3112 invalid_chars = '\/:*?"<>|'
3113 
3114 def clean_object_name(value):
3115  global invalid_chars
3116  for invalid_char in invalid_chars:
3117  value = value.replace(invalid_char, '_')
3118  value = value.replace(' ', '_')
3119  return value;
3120 
3121 def clean_object_name_with_spaces(value):
3122  global invalid_chars
3123  for invalid_char in invalid_chars:
3124  value = value.replace(invalid_char, '_')
3125  return value;
3126 
3127 last_export_filepath = ""
3128 
3129 class _OgreCommonExport_(_TXML_):
3130 
3131  @classmethod
3132  def poll(cls, context):
3133  if context.active_object and context.mode != 'EDIT_MESH':
3134  return True
3135 
3136  def invoke(self, context, event):
3137  # Resolve path from opened .blend if available. It's not if
3138  # blender normally was opened with "last open scene".
3139  # After export is done once, remember that path when re-exporting.
3140  global last_export_filepath
3141  if last_export_filepath == "":
3142  # First export during this blender run
3143  if self.filepath == "" and context.blend_data.filepath != "":
3144  path, name = os.path.split(context.blend_data.filepath)
3145  self.filepath = os.path.join(path, name.split('.')[0])
3146  if self.filepath == "":
3147  self.filepath = "blender2ogre-export"
3148  if self.EXPORT_TYPE == "OGRE":
3149  self.filepath += ".scene"
3150  elif self.EXPORT_TYPE == "REX":
3151  self.filepath += ".txml"
3152  else:
3153  # Sequential export, use the previous path
3154  self.filepath = last_export_filepath
3155 
3156  # Replace file extension if we have swapped dialogs.
3157  if self.EXPORT_TYPE == "OGRE":
3158  self.filepath = self.filepath.replace(".txml", ".scene")
3159  elif self.EXPORT_TYPE == "REX":
3160  self.filepath = self.filepath.replace(".scene", ".txml")
3161 
3162  # Update ui setting from the last export, or file config.
3163  self.update_ui()
3164 
3165  wm = context.window_manager
3166  fs = wm.fileselect_add(self) # writes to filepath
3167  return {'RUNNING_MODAL'}
3168 
3169  def execute(self, context):
3170  # Store this path for later re-export
3171  global last_export_filepath
3172  last_export_filepath = self.filepath
3173 
3174  # Run the .scene or .txml export
3175  self.ogre_export(self.filepath, context)
3176  return {'FINISHED'}
3177 
3178  def update_ui(self):
3179  self.EX_SWAP_AXIS = CONFIG['SWAP_AXIS']
3180  self.EX_SEP_MATS = CONFIG['SEP_MATS']
3181  self.EX_ONLY_DEFORMABLE_BONES = CONFIG['ONLY_DEFORMABLE_BONES']
3182  self.EX_ONLY_KEYFRAMED_BONES = CONFIG['ONLY_KEYFRAMED_BONES']
3183  self.EX_OGRE_INHERIT_SCALE = CONFIG['OGRE_INHERIT_SCALE']
3184  self.EX_SCENE = CONFIG['SCENE']
3185  self.EX_EXPORT_HIDDEN = CONFIG['EXPORT_HIDDEN']
3186  self.EX_SELONLY = CONFIG['SELONLY']
3187  self.EX_FORCE_CAMERA = CONFIG['FORCE_CAMERA']
3188  self.EX_FORCE_LAMPS = CONFIG['FORCE_LAMPS']
3189  self.EX_MESH = CONFIG['MESH']
3190  self.EX_MESH_OVERWRITE = CONFIG['MESH_OVERWRITE']
3191  self.EX_ARM_ANIM = CONFIG['ARM_ANIM']
3192  self.EX_SHAPE_ANIM = CONFIG['SHAPE_ANIM']
3193  self.EX_TRIM_BONE_WEIGHTS = CONFIG['TRIM_BONE_WEIGHTS']
3194  self.EX_ARRAY = CONFIG['ARRAY']
3195  self.EX_MATERIALS = CONFIG['MATERIALS']
3196  self.EX_FORCE_IMAGE_FORMAT = CONFIG['FORCE_IMAGE_FORMAT']
3197  self.EX_DDS_MIPS = CONFIG['DDS_MIPS']
3198  self.EX_COPY_SHADER_PROGRAMS = CONFIG['COPY_SHADER_PROGRAMS']
3199  self.EX_lodLevels = CONFIG['lodLevels']
3200  self.EX_lodDistance = CONFIG['lodDistance']
3201  self.EX_lodPercent = CONFIG['lodPercent']
3202  self.EX_nuextremityPoints = CONFIG['nuextremityPoints']
3203  self.EX_generateEdgeLists = CONFIG['generateEdgeLists']
3204  self.EX_generateTangents = CONFIG['generateTangents']
3205  self.EX_tangentSemantic = CONFIG['tangentSemantic']
3206  self.EX_tangentUseParity = CONFIG['tangentUseParity']
3207  self.EX_tangentSplitMirrored = CONFIG['tangentSplitMirrored']
3208  self.EX_tangentSplitRotated = CONFIG['tangentSplitRotated']
3209  self.EX_reorganiseBuffers = CONFIG['reorganiseBuffers']
3210  self.EX_optimiseAnimations = CONFIG['optimiseAnimations']
3211 
3212  # Basic options
3213  EX_SWAP_AXIS = EnumProperty(
3214  items=AXIS_MODES,
3215  name='swap axis',
3216  description='axis swapping mode',
3217  default= CONFIG['SWAP_AXIS'])
3218  EX_SEP_MATS = BoolProperty(
3219  name="Separate Materials",
3220  description="exports a .material for each material (rather than putting all materials in a single .material file)",
3221  default=CONFIG['SEP_MATS'])
3222  EX_ONLY_DEFORMABLE_BONES = BoolProperty(
3223  name="Only Deformable Bones",
3224  description="only exports bones that are deformable. Useful for hiding IK-Bones used in Blender. Note: Any bone with deformable children/descendants will be output as well.",
3225  default=CONFIG['ONLY_DEFORMABLE_BONES'])
3226  EX_ONLY_KEYFRAMED_BONES = BoolProperty(
3227  name="Only Keyframed Bones",
3228  description="only exports bones that have been keyframed for a given animation. Useful to limit the set of bones on a per-animation basis.",
3229  default=CONFIG['ONLY_KEYFRAMED_BONES'])
3230  EX_OGRE_INHERIT_SCALE = BoolProperty(
3231  name="OGRE inherit scale",
3232  description="whether the OGRE bones have the 'inherit scale' flag on. If the animation has scale in it, the exported animation needs to be adjusted to account for the state of the inherit-scale flag in OGRE.",
3233  default=CONFIG['OGRE_INHERIT_SCALE'])
3234  EX_SCENE = BoolProperty(
3235  name="Export Scene",
3236  description="export current scene (OgreDotScene xml)",
3237  default=CONFIG['SCENE'])
3238  EX_SELONLY = BoolProperty(
3239  name="Export Selected Only",
3240  description="export selected",
3241  default=CONFIG['SELONLY'])
3242  EX_EXPORT_HIDDEN = BoolProperty(
3243  name="Export Hidden Also",
3244  description="Export hidden meshes in addition to visible ones. Turn off to avoid exporting hidden stuff.",
3245  default=CONFIG['EXPORT_HIDDEN'])
3246  EX_FORCE_CAMERA = BoolProperty(
3247  name="Force Camera",
3248  description="export active camera",
3249  default=CONFIG['FORCE_CAMERA'])
3250  EX_FORCE_LAMPS = BoolProperty(
3251  name="Force Lamps",
3252  description="export all lamps",
3253  default=CONFIG['FORCE_LAMPS'])
3254  EX_MESH = BoolProperty(
3255  name="Export Meshes",
3256  description="export meshes",
3257  default=CONFIG['MESH'])
3258  EX_MESH_OVERWRITE = BoolProperty(
3259  name="Export Meshes (overwrite)",
3260  description="export meshes (overwrite existing files)",
3261  default=CONFIG['MESH_OVERWRITE'])
3262  EX_ARM_ANIM = BoolProperty(
3263  name="Armature Animation",
3264  description="export armature animations - updates the .skeleton file",
3265  default=CONFIG['ARM_ANIM'])
3266  EX_SHAPE_ANIM = BoolProperty(
3267  name="Shape Animation",
3268  description="export shape animations - updates the .mesh file",
3269  default=CONFIG['SHAPE_ANIM'])
3270  EX_TRIM_BONE_WEIGHTS = FloatProperty(
3271  name="Trim Weights",
3272  description="ignore bone weights below this value (Ogre supports 4 bones per vertex)",
3273  min=0.0, max=0.5, default=CONFIG['TRIM_BONE_WEIGHTS'] )
3274  EX_ARRAY = BoolProperty(
3275  name="Optimize Arrays",
3276  description="optimize array modifiers as instances (constant offset only)",
3277  default=CONFIG['ARRAY'])
3278  EX_MATERIALS = BoolProperty(
3279  name="Export Materials",
3280  description="exports .material script",
3281  default=CONFIG['MATERIALS'])
3282  EX_FORCE_IMAGE_FORMAT = EnumProperty(
3283  items=_IMAGE_FORMATS,
3284  name='Convert Images',
3285  description='convert all textures to format',
3286  default=CONFIG['FORCE_IMAGE_FORMAT'] )
3287  EX_DDS_MIPS = IntProperty(
3288  name="DDS Mips",
3289  description="number of mip maps (DDS)",
3290  min=0, max=16,
3291  default=CONFIG['DDS_MIPS'])
3292 
3293  # Mesh options
3294  EX_lodLevels = IntProperty(
3295  name="LOD Levels",
3296  description="MESH number of LOD levels",
3297  min=0, max=32,
3298  default=CONFIG['lodLevels'])
3299  EX_lodDistance = IntProperty(
3300  name="LOD Distance",
3301  description="MESH distance increment to reduce LOD",
3302  min=0, max=2000, default=CONFIG['lodDistance'])
3303  EX_lodPercent = IntProperty(
3304  name="LOD Percentage",
3305  description="LOD percentage reduction",
3306  min=0, max=99,
3307  default=CONFIG['lodPercent'])
3308  EX_nuextremityPoints = IntProperty(
3309  name="Extremity Points",
3310  description="MESH Extremity Points",
3311  min=0, max=65536,
3312  default=CONFIG['nuextremityPoints'])
3313  EX_generateEdgeLists = BoolProperty(
3314  name="Edge Lists",
3315  description="MESH generate edge lists (for stencil shadows)",
3316  default=CONFIG['generateEdgeLists'])
3317  EX_generateTangents = BoolProperty(
3318  name="Tangents",
3319  description="MESH generate tangents",
3320  default=CONFIG['generateTangents'])
3321  EX_tangentSemantic = StringProperty(
3322  name="Tangent Semantic",
3323  description="MESH tangent semantic - can be 'uvw' or 'tangent'",
3324  maxlen=16,
3325  default=CONFIG['tangentSemantic'])
3326  EX_tangentUseParity = IntProperty(
3327  name="Tangent Parity",
3328  description="MESH tangent use parity",
3329  min=0, max=16,
3330  default=CONFIG['tangentUseParity'])
3331  EX_tangentSplitMirrored = BoolProperty(
3332  name="Tangent Split Mirrored",
3333  description="MESH split mirrored tangents",
3334  default=CONFIG['tangentSplitMirrored'])
3335  EX_tangentSplitRotated = BoolProperty(
3336  name="Tangent Split Rotated",
3337  description="MESH split rotated tangents",
3338  default=CONFIG['tangentSplitRotated'])
3339  EX_reorganiseBuffers = BoolProperty(
3340  name="Reorganise Buffers",
3341  description="MESH reorganise vertex buffers",
3342  default=CONFIG['reorganiseBuffers'])
3343  EX_optimiseAnimations = BoolProperty(
3344  name="Optimize Animations",
3345  description="MESH optimize animations",
3346  default=CONFIG['optimiseAnimations'])
3347  EX_COPY_SHADER_PROGRAMS = BoolProperty(
3348  name="copy shader programs",
3349  description="when using script inheritance copy the source shader programs to the output path",
3350  default=CONFIG['COPY_SHADER_PROGRAMS'])
3351 
3352  filepath_last = ""
3353  filepath = StringProperty(
3354  name="File Path",
3355  description="Filepath used for exporting file",
3356  maxlen=1024, default="",
3357  subtype='FILE_PATH')
3358 
3359  def dot_material( self, meshes, path='/tmp', mat_file_name='SceneMaterial'):
3360  material_files = []
3361  mats = []
3362  for ob in meshes:
3363  if len(ob.data.materials):
3364  for mat in ob.data.materials:
3365  if mat not in mats:
3366  mats.append( mat )
3367 
3368  if not mats:
3369  print('WARNING: no materials, not writting .material script'); return []
3370 
3371  M = MISSING_MATERIAL + '\n'
3372  for mat in mats:
3373  if mat is None:
3374  continue
3375  Report.materials.append( material_name(mat) )
3376  if CONFIG['COPY_SHADER_PROGRAMS']:
3377  data = generate_material( mat, path=path, copy_programs=True, touch_textures=CONFIG['TOUCH_TEXTURES'] )
3378  else:
3379  data = generate_material( mat, path=path, touch_textures=CONFIG['TOUCH_TEXTURES'] )
3380 
3381  M += data
3382  # Write own .material file per material
3383  if self.EX_SEP_MATS:
3384  url = self.dot_material_write_separate( mat, data, path )
3385  material_files.append(url)
3386 
3387  # Write one .material file for everything
3388  if not self.EX_SEP_MATS:
3389  try:
3390  url = os.path.join(path, '%s.material' % mat_file_name)
3391  f = open( url, 'wb' ); f.write( bytes(M,'utf-8') ); f.close()
3392  print(' - Created material:', url)
3393  material_files.append( url )
3394  except Exception as e:
3395  show_dialog("Invalid material object name: " + mat_file_name)
3396 
3397  return material_files
3398 
3399  def dot_material_write_separate( self, mat, data, path = '/tmp' ):
3400  try:
3401  clean_filename = clean_object_name(mat.name);
3402  url = os.path.join(path, '%s.material' % clean_filename)
3403  f = open(url, 'wb'); f.write( bytes(data,'utf-8') ); f.close()
3404  print(' - Exported Material:', url)
3405  return url
3406  except Exception as e:
3407  show_dialog("Invalid material object name: " + clean_filename)
3408  return ""
3409 
3410  def dot_mesh( self, ob, path='/tmp', force_name=None, ignore_shape_animation=False ):
3411  dot_mesh( ob, path, force_name, ignore_shape_animation=False )
3412 
3413  def ogre_export(self, url, context, force_material_update=[]):
3414  print ("_"*80)
3415 
3416  # Updating config to latest values?
3417  global CONFIG
3418  for name in dir(self):
3419  if name.startswith('EX_'):
3420  CONFIG[ name[3:] ] = getattr(self,name)
3421 
3422  Report.reset()
3423 
3424  print("Processing Scene")
3425  prefix = url.split('.')[0]
3426  path = os.path.split(url)[0]
3427 
3428  # Nodes (objects) - gather because macros will change selection state
3429  objects = []
3430  linkedgroups = []
3431  invalidnamewarnings = []
3432  for ob in bpy.context.scene.objects:
3433  if ob.subcollision:
3434  continue
3435  if not self.EX_EXPORT_HIDDEN and ob.hide:
3436  continue
3437  if self.EX_SELONLY and not ob.select:
3438  if ob.type == 'CAMERA' and self.EX_FORCE_CAMERA:
3439  pass
3440  elif ob.type == 'LAMP' and self.EX_FORCE_LAMPS:
3441  pass
3442  else:
3443  continue
3444  if ob.type == 'EMPTY' and ob.dupli_group and ob.dupli_type == 'GROUP':
3445  linkedgroups.append(ob)
3446  else:
3447  # Gather data of invalid names. Don't bother user with warnings on names
3448  # that only get spaces converted to _, just do that automatically.
3449  cleanname = clean_object_name(ob.name)
3450  cleannamespaces = clean_object_name_with_spaces(ob.name)
3451  if cleanname != ob.name:
3452  if cleannamespaces != ob.name:
3453  invalidnamewarnings.append(ob.name + " -> " + cleanname)
3454  objects.append(ob)
3455 
3456  # Print invalid obj names so user can go and fix them.
3457  if len(invalidnamewarnings) > 0:
3458  print ("[Warning]: Following object names have invalid characters for creating files. They will be automatically converted.")
3459  for namewarning in invalidnamewarnings:
3460  Report.warnings.append("Auto correcting object name: " + namewarning)
3461  print (" - ", namewarning)
3462 
3463  # Linked groups - allows 3 levels of nested blender library linking
3464  temps = []
3465  for e in linkedgroups:
3466  grp = e.dupli_group
3467  subs = []
3468  for o in grp.objects:
3469  if o.type=='MESH':
3470  subs.append( o ) # TOP-LEVEL
3471  elif o.type == 'EMPTY' and o.dupli_group and o.dupli_type == 'GROUP':
3472  ss = [] # LEVEL2
3473  for oo in o.dupli_group.objects:
3474  if oo.type=='MESH':
3475  ss.append( oo )
3476  elif oo.type == 'EMPTY' and oo.dupli_group and oo.dupli_type == 'GROUP':
3477  sss = [] # LEVEL3
3478  for ooo in oo.dupli_group.objects:
3479  if ooo.type=='MESH':
3480  sss.append( ooo )
3481  if sss:
3482  m = merge_objects( sss, name=oo.name, transform=oo.matrix_world )
3483  subs.append( m )
3484  temps.append( m )
3485  if ss:
3486  m = merge_objects( ss, name=o.name, transform=o.matrix_world )
3487  subs.append( m )
3488  temps.append( m )
3489  if subs:
3490  m = merge_objects( subs, name=e.name, transform=e.matrix_world )
3491  objects.append( m )
3492  temps.append( m )
3493 
3494  # Find merge groups
3495  mgroups = []
3496  mobjects = []
3497  for ob in objects:
3498  group = get_merge_group( ob )
3499  if group:
3500  for member in group.objects:
3501  if member not in mobjects: mobjects.append( member )
3502  if group not in mgroups: mgroups.append( group )
3503  for rem in mobjects:
3504  if rem in objects: objects.remove( rem )
3505 
3506  for group in mgroups:
3507  merged = merge_group( group )
3508  objects.append( merged )
3509  temps.append( merged )
3510 
3511  # Gather roots because ogredotscene supports parents and children
3512  def _flatten( _c, _f ):
3513  if _c.parent in objects: _f.append( _c.parent )
3514  if _c.parent: _flatten( _c.parent, _f )
3515  else: _f.append( _c )
3516 
3517  roots = []
3518  meshes = []
3519 
3520  for ob in objects:
3521  flat = []
3522  _flatten( ob, flat )
3523  root = flat[-1]
3524  if root not in roots:
3525  roots.append(root)
3526  if ob.type=='MESH':
3527  meshes.append(ob)
3528 
3529  mesh_collision_prims = {}
3530  mesh_collision_files = {}
3531 
3532  # Track that we don't export same data multiple times
3533  exported_meshes = []
3534 
3535  if self.EX_MATERIALS:
3536  print (" Processing Materials")
3537  material_file_name_base = os.path.split(url)[1].replace('.scene', '').replace('.txml', '')
3538  material_files = self.dot_material(meshes + force_material_update, path, material_file_name_base)
3539  else:
3540  material_files = []
3541 
3542  # realXtend Tundra .txml scene description export
3543  if self.EXPORT_TYPE == 'REX':
3544  rex = self.create_tundra_document(context)
3545  proxies = []
3546  for ob in objects:
3547  print(" Processing %s [%s]" % (ob.name, ob.type))
3548 
3549  # This seemingly needs to be done as its done in .scene
3550  # export. Fixed a bug that no .meshes were exported when doing
3551  # a Tundra export.
3552  if ob.type == 'MESH':
3553  ob.data.update(calc_tessface=True)
3554 
3555  # EC_Light
3556  if ob.type == 'LAMP':
3557  TE = self.tundra_entity(rex, ob, path=path, collision_proxies=proxies)
3558  self.tundra_light( TE, ob )
3559  # EC_Sound
3560  elif ob.type == 'SPEAKER':
3561  TE = self.tundra_entity(rex, ob, path=path, collision_proxies=proxies)
3562  # EC_Mesh
3563  elif ob.type == 'MESH' and len(ob.data.tessfaces):
3564  if ob.modifiers and ob.modifiers[0].type=='MULTIRES' and ob.use_multires_lod:
3565  mod = ob.modifiers[0]
3566  basename = ob.name
3567  dataname = ob.data.name
3568  ID = uid( ob ) # ensure uid
3569  TE = self.tundra_entity(rex, ob, path=path, collision_proxies=proxies)
3570 
3571  for level in range( mod.total_levels+1 ):
3572  ob.uid += 1
3573  mod.levels = level
3574  ob.name = '%s.LOD%s' %(basename,level)
3575  ob.data.name = '%s.LOD%s' %(dataname,level)
3576  TE = self.tundra_entity(
3577  rex, ob, path=path, collision_proxies=proxies, parent=basename,
3578  matrix=mathutils.Matrix(), visible=False
3579  )
3580  self.tundra_mesh( TE, ob, url, exported_meshes )
3581 
3582  ob.uid = ID
3583  ob.name = basename
3584  ob.data.name = dataname
3585  else:
3586  TE = self.tundra_entity( rex, ob, path=path, collision_proxies=proxies )
3587  self.tundra_mesh( TE, ob, url, exported_meshes )
3588 
3589  # EC_RigidBody separate collision meshes
3590  for proxy in proxies:
3591  self.dot_mesh(
3592  proxy,
3593  path=os.path.split(url)[0],
3594  force_name='_collision_%s' %proxy.data.name
3595  )
3596 
3597  if self.EX_SCENE:
3598  if not url.endswith('.txml'):
3599  url += '.txml'
3600  data = rex.toprettyxml()
3601  f = open( url, 'wb' ); f.write( bytes(data,'utf-8') ); f.close()
3602  print(' Exported Tundra Scene:', url)
3603 
3604  # Ogre .scene scene description export
3605  elif self.EXPORT_TYPE == 'OGRE':
3606  doc = self.create_ogre_document( context, material_files )
3607 
3608  for root in roots:
3609  print(' - Exporting root node:', root.name)
3610  self._node_export(
3611  root,
3612  url = url,
3613  doc = doc,
3614  exported_meshes = exported_meshes,
3615  meshes = meshes,
3616  mesh_collision_prims = mesh_collision_prims,
3617  mesh_collision_files = mesh_collision_files,
3618  prefix = prefix,
3619  objects = objects,
3620  xmlparent = doc._scene_nodes
3621  )
3622 
3623  if self.EX_SCENE:
3624  if not url.endswith('.scene'):
3625  url += '.scene'
3626  data = doc.toprettyxml()
3627  f = open( url, 'wb' ); f.write( bytes(data,'utf-8') ); f.close()
3628  print(' Exported Ogre Scene:', url)
3629 
3630  for ob in temps:
3631  context.scene.objects.unlink( ob )
3632  bpy.ops.wm.call_menu(name='MiniReport')
3633 
3634  # Always save?
3635  # todo: This does not seem to stick! It might save to disk
3636  # but the old config defaults are read when this panel is opened!
3637  save_config()
3638 
3639  def create_ogre_document(self, context, material_files=[] ):
3640  now = time.time()
3641  doc = RDocument()
3642  scn = doc.createElement('scene'); doc.appendChild( scn )
3643  scn.setAttribute('export_time', str(now))
3644  scn.setAttribute('formatVersion', '1.0.1')
3645  bscn = bpy.context.scene
3646 
3647  if '_previous_export_time_' in bscn.keys():
3648  scn.setAttribute('previous_export_time', str(bscn['_previous_export_time_']))
3649  else:
3650  scn.setAttribute('previous_export_time', '0')
3651  bscn[ '_previous_export_time_' ] = now
3652  scn.setAttribute('exported_by', getpass.getuser())
3653 
3654  nodes = doc.createElement('nodes')
3655  doc._scene_nodes = nodes
3656  extern = doc.createElement('externals')
3657  environ = doc.createElement('environment')
3658  for n in (nodes,extern,environ):
3659  scn.appendChild( n )
3660 
3661  # Extern files
3662  for url in material_files:
3663  item = doc.createElement('item'); extern.appendChild( item )
3664  item.setAttribute('type','material')
3665  a = doc.createElement('file'); item.appendChild( a )
3666  a.setAttribute('name', url)
3667 
3668  # Environ settings
3669  world = context.scene.world
3670  if world: # multiple scenes - other scenes may not have a world
3671  _c = {'colourAmbient':world.ambient_color, 'colourBackground':world.horizon_color, 'colourDiffuse':world.horizon_color}
3672  for ctag in _c:
3673  a = doc.createElement(ctag); environ.appendChild( a )
3674  color = _c[ctag]
3675  a.setAttribute('r', '%s'%color.r)
3676  a.setAttribute('g', '%s'%color.g)
3677  a.setAttribute('b', '%s'%color.b)
3678 
3679  if world and world.mist_settings.use_mist:
3680  a = doc.createElement('fog'); environ.appendChild( a )
3681  a.setAttribute('linearStart', '%s'%world.mist_settings.start )
3682  mist_falloff = world.mist_settings.falloff
3683  if mist_falloff == 'QUADRATIC': a.setAttribute('mode', 'exp') # on DTD spec (none | exp | exp2 | linear)
3684  elif mist_falloff == 'LINEAR': a.setAttribute('mode', 'linear')
3685  else: a.setAttribute('mode', 'exp2')
3686  #a.setAttribute('mode', world.mist_settings.falloff.lower() ) # not on DTD spec
3687  a.setAttribute('linearEnd', '%s' %(world.mist_settings.start+world.mist_settings.depth))
3688  a.setAttribute('expDensity', world.mist_settings.intensity)
3689  a.setAttribute('colourR', world.horizon_color.r)
3690  a.setAttribute('colourG', world.horizon_color.g)
3691  a.setAttribute('colourB', world.horizon_color.b)
3692 
3693  return doc
3694 
3695  # Recursive Node export
3696  def _node_export( self, ob, url='', doc=None, rex=None, exported_meshes=[], meshes=[], mesh_collision_prims={}, mesh_collision_files={}, prefix='', objects=[], xmlparent=None ):
3697  o = _ogre_node_helper( doc, ob, objects )
3698  xmlparent.appendChild(o)
3699 
3700  # Custom user props
3701  for prop in ob.items():
3702  propname, propvalue = prop
3703  if not propname.startswith('_'):
3704  user = doc.createElement('user_data')
3705  o.appendChild( user )
3706  user.setAttribute( 'name', propname )
3707  user.setAttribute( 'value', str(propvalue) )
3708  user.setAttribute( 'type', type(propvalue).__name__ )
3709 
3710  # Custom user props from BGE props by Mind Calamity
3711  for prop in ob.game.properties:
3712  e = doc.createElement( 'user_data' )
3713  o.appendChild( e )
3714  e.setAttribute('name', prop.name)
3715  e.setAttribute('value', str(prop.value))
3716  e.setAttribute('type', type(prop.value).__name__)
3717  # -- end of Mind Calamity patch
3718 
3719  # BGE subset
3720  game = doc.createElement('game')
3721  o.appendChild( game )
3722  sens = doc.createElement('sensors')
3723  game.appendChild( sens )
3724  acts = doc.createElement('actuators')
3725  game.appendChild( acts )
3726  for sen in ob.game.sensors:
3727  sens.appendChild( WrapSensor(sen).xml(doc) )
3728  for act in ob.game.actuators:
3729  acts.appendChild( WrapActuator(act).xml(doc) )
3730 
3731  if ob.type == 'MESH':
3732  ob.data.update(calc_tessface=True)
3733 
3734  if ob.type == 'MESH' and len(ob.data.tessfaces):
3735  collisionFile = None
3736  collisionPrim = None
3737  if ob.data.name in mesh_collision_prims:
3738  collisionPrim = mesh_collision_prims[ ob.data.name ]
3739  if ob.data.name in mesh_collision_files:
3740  collisionFile = mesh_collision_files[ ob.data.name ]
3741 
3742  e = doc.createElement('entity')
3743  o.appendChild(e); e.setAttribute('name', ob.data.name)
3744  prefix = ''
3745  e.setAttribute('meshFile', '%s%s.mesh' %(prefix,ob.data.name) )
3746 
3747  if not collisionPrim and not collisionFile:
3748  if ob.game.use_collision_bounds:
3749  collisionPrim = ob.game.collision_bounds_type.lower()
3750  mesh_collision_prims[ ob.data.name ] = collisionPrim
3751  else:
3752  for child in ob.children:
3753  if child.subcollision and child.name.startswith('DECIMATE'):
3754  collisionFile = '%s_collision_%s.mesh' %(prefix,ob.data.name)
3755  break
3756  if collisionFile:
3757  mesh_collision_files[ ob.data.name ] = collisionFile
3758  self.dot_mesh(
3759  child,
3760  path=os.path.split(url)[0],
3761  force_name='_collision_%s' %ob.data.name
3762  )
3763 
3764  if collisionPrim:
3765  e.setAttribute('collisionPrim', collisionPrim )
3766  elif collisionFile:
3767  e.setAttribute('collisionFile', collisionFile )
3768 
3769  _mesh_entity_helper( doc, ob, e )
3770 
3771  if self.EX_MESH:
3772  murl = os.path.join( os.path.split(url)[0], '%s.mesh'%ob.data.name )
3773  exists = os.path.isfile( murl )
3774  if not exists or (exists and self.EX_MESH_OVERWRITE):
3775  if ob.data.name not in exported_meshes:
3776  exported_meshes.append( ob.data.name )
3777  self.dot_mesh( ob, os.path.split(url)[0] )
3778 
3779  # Deal with Array modifier
3780  vecs = [ ob.matrix_world.to_translation() ]
3781  for mod in ob.modifiers:
3782  if mod.type == 'ARRAY':
3783  if mod.fit_type != 'FIXED_COUNT':
3784  print( 'WARNING: unsupport array-modifier type->', mod.fit_type )
3785  continue
3786  if not mod.use_constant_offset:
3787  print( 'WARNING: unsupport array-modifier mode, must be "constant offset" type' )
3788  continue
3789  else:
3790  #v = ob.matrix_world.to_translation()
3791  newvecs = []
3792  for prev in vecs:
3793  for i in range( mod.count-1 ):
3794  v = prev + mod.constant_offset_displace
3795  newvecs.append( v )
3796  ao = _ogre_node_helper( doc, ob, objects, prefix='_array_%s_'%len(vecs+newvecs), pos=v )
3797  xmlparent.appendChild(ao)
3798 
3799  e = doc.createElement('entity')
3800  ao.appendChild(e); e.setAttribute('name', ob.data.name)
3801  #if self.EX_MESH_SUBDIR: e.setAttribute('meshFile', 'meshes/%s.mesh' %ob.data.name)
3802  #else:
3803  e.setAttribute('meshFile', '%s.mesh' %ob.data.name)
3804 
3805  if collisionPrim: e.setAttribute('collisionPrim', collisionPrim )
3806  elif collisionFile: e.setAttribute('collisionFile', collisionFile )
3807  vecs += newvecs
3808 
3809  elif ob.type == 'CAMERA':
3810  Report.cameras.append( ob.name )
3811  c = doc.createElement('camera')
3812  o.appendChild(c); c.setAttribute('name', ob.data.name)
3813  aspx = bpy.context.scene.render.pixel_aspect_x
3814  aspy = bpy.context.scene.render.pixel_aspect_y
3815  sx = bpy.context.scene.render.resolution_x
3816  sy = bpy.context.scene.render.resolution_y
3817  fovY = 0.0
3818  if (sx*aspx > sy*aspy):
3819  fovY = 2*math.atan(sy*aspy*16.0/(ob.data.lens*sx*aspx))
3820  else:
3821  fovY = 2*math.atan(16.0/ob.data.lens)
3822  # fov in radians - like OgreMax - requested by cyrfer
3823  fov = math.radians( fovY*180.0/math.pi )
3824  c.setAttribute('fov', '%s'%fov)
3825  c.setAttribute('projectionType', "perspective")
3826  a = doc.createElement('clipping'); c.appendChild( a )
3827  a.setAttribute('nearPlaneDist', '%s' %ob.data.clip_start)
3828  a.setAttribute('farPlaneDist', '%s' %ob.data.clip_end)
3829  a.setAttribute('near', '%s' %ob.data.clip_start) # requested by cyrfer
3830  a.setAttribute('far', '%s' %ob.data.clip_end)
3831 
3832  elif ob.type == 'LAMP' and ob.data.type in 'POINT SPOT SUN'.split():
3833  Report.lights.append( ob.name )
3834  l = doc.createElement('light')
3835  o.appendChild(l)
3836 
3837  mat = get_parent_matrix(ob, objects).inverted() * ob.matrix_world
3838 
3839  p = doc.createElement('position') # just to make sure we conform with the DTD
3840  l.appendChild(p)
3841  v = swap( ob.matrix_world.to_translation() )
3842  p.setAttribute('x', '%6f'%v.x)
3843  p.setAttribute('y', '%6f'%v.y)
3844  p.setAttribute('z', '%6f'%v.z)
3845 
3846  if ob.data.type == 'POINT':
3847  l.setAttribute('type', 'point')
3848  elif ob.data.type == 'SPOT':
3849  l.setAttribute('type', 'spot')
3850  elif ob.data.type == 'SUN':
3851  l.setAttribute('type', 'directional')
3852 
3853  l.setAttribute('name', ob.name )
3854  l.setAttribute('powerScale', str(ob.data.energy))
3855 
3856  a = doc.createElement('lightAttenuation'); l.appendChild( a )
3857  a.setAttribute('range', '5000' ) # is this an Ogre constant?
3858  a.setAttribute('constant', '1.0') # TODO support quadratic light
3859  a.setAttribute('linear', '%s'%(1.0/ob.data.distance))
3860  a.setAttribute('quadratic', '0.0')
3861 
3862  if ob.data.type in ('SPOT', 'SUN'):
3863  vector = swap(mathutils.Euler.to_matrix(ob.rotation_euler)[2])
3864  a = doc.createElement('direction')
3865  l.appendChild(a)
3866  a.setAttribute('x',str(round(-vector[0],3)))
3867  a.setAttribute('y',str(round(-vector[1],3)))
3868  a.setAttribute('z',str(round(-vector[2],3)))
3869 
3870  if ob.data.type == 'SPOT':
3871  a = doc.createElement('spotLightRange')
3872  l.appendChild(a)
3873  a.setAttribute('inner',str( ob.data.spot_size*(1.0-ob.data.spot_blend) ))
3874  a.setAttribute('outer',str(ob.data.spot_size))
3875  a.setAttribute('falloff','1.0')
3876 
3877  if ob.data.use_diffuse:
3878  a = doc.createElement('colourDiffuse'); l.appendChild( a )
3879  a.setAttribute('r', '%s'%ob.data.color.r)
3880  a.setAttribute('g', '%s'%ob.data.color.g)
3881  a.setAttribute('b', '%s'%ob.data.color.b)
3882 
3883  if ob.data.use_specular:
3884  a = doc.createElement('colourSpecular'); l.appendChild( a )
3885  a.setAttribute('r', '%s'%ob.data.color.r)
3886  a.setAttribute('g', '%s'%ob.data.color.g)
3887  a.setAttribute('b', '%s'%ob.data.color.b)
3888 
3889  if ob.data.type != 'HEMI': # colourShadow is extra, not part of Ogre DTD
3890  if ob.data.shadow_method != 'NOSHADOW': # Hemi light has no shadow_method
3891  a = doc.createElement('colourShadow');l.appendChild( a )
3892  a.setAttribute('r', '%s'%ob.data.color.r)
3893  a.setAttribute('g', '%s'%ob.data.color.g)
3894  a.setAttribute('b', '%s'%ob.data.color.b)
3895  l.setAttribute('shadow','true')
3896 
3897  for child in ob.children:
3898  self._node_export( child,
3899  url = url, doc = doc, rex = rex,
3900  exported_meshes = exported_meshes,
3901  meshes = meshes,
3902  mesh_collision_prims = mesh_collision_prims,
3903  mesh_collision_files = mesh_collision_files,
3904  prefix = prefix,
3905  objects=objects,
3906  xmlparent=o
3907  )
3908 
3909 ## UI panel Ogre export - Subclasses _OgreCommonExport_
3910 
3911 class INFO_OT_createOgreExport(bpy.types.Operator, _OgreCommonExport_):
3912  '''Export Ogre Scene'''
3913  bl_idname = "ogre.export"
3914  bl_label = "Export Ogre"
3915  bl_options = {'REGISTER'}
3916 
3917  # Basic options
3918  EX_SWAP_AXIS = EnumProperty(
3919  items=AXIS_MODES,
3920  name='swap axis',
3921  description='axis swapping mode',
3922  default= CONFIG['SWAP_AXIS'])
3923  EX_SEP_MATS = BoolProperty(
3924  name="Separate Materials",
3925  description="exports a .material for each material (rather than putting all materials in a single .material file)",
3926  default=CONFIG['SEP_MATS'])
3927  EX_ONLY_DEFORMABLE_BONES = BoolProperty(
3928  name="Only Deformable Bones",
3929  description="only exports bones that are deformable. Useful for hiding IK-Bones used in Blender. Note: Any bone with deformable children/descendants will be output as well.",
3930  default=CONFIG['ONLY_DEFORMABLE_BONES'])
3931  EX_ONLY_KEYFRAMED_BONES = BoolProperty(
3932  name="Only Keyframed Bones",
3933  description="only exports bones that have been keyframed for a given animation. Useful to limit the set of bones on a per-animation basis.",
3934  default=CONFIG['ONLY_KEYFRAMED_BONES'])
3935  EX_OGRE_INHERIT_SCALE = BoolProperty(
3936  name="OGRE inherit scale",
3937  description="whether the OGRE bones have the 'inherit scale' flag on. If the animation has scale in it, the exported animation needs to be adjusted to account for the state of the inherit-scale flag in OGRE.",
3938  default=CONFIG['OGRE_INHERIT_SCALE'])
3939  EX_SCENE = BoolProperty(
3940  name="Export Scene",
3941  description="export current scene (OgreDotScene xml)",
3942  default=CONFIG['SCENE'])
3943  EX_SELONLY = BoolProperty(
3944  name="Export Selected Only",
3945  description="export selected",
3946  default=CONFIG['SELONLY'])
3947  EX_EXPORT_HIDDEN = BoolProperty(
3948  name="Export Hidden Also",
3949  description="Export hidden meshes in addition to visible ones. Turn off to avoid exporting hidden stuff.",
3950  default=CONFIG['EXPORT_HIDDEN'])
3951  EX_FORCE_CAMERA = BoolProperty(
3952  name="Force Camera",
3953  description="export active camera",
3954  default=CONFIG['FORCE_CAMERA'])
3955  EX_FORCE_LAMPS = BoolProperty(
3956  name="Force Lamps",
3957  description="export all lamps",
3958  default=CONFIG['FORCE_LAMPS'])
3959  EX_MESH = BoolProperty(
3960  name="Export Meshes",
3961  description="export meshes",
3962  default=CONFIG['MESH'])
3963  EX_MESH_OVERWRITE = BoolProperty(
3964  name="Export Meshes (overwrite)",
3965  description="export meshes (overwrite existing files)",
3966  default=CONFIG['MESH_OVERWRITE'])
3967  EX_ARM_ANIM = BoolProperty(
3968  name="Armature Animation",
3969  description="export armature animations - updates the .skeleton file",
3970  default=CONFIG['ARM_ANIM'])
3971  EX_SHAPE_ANIM = BoolProperty(
3972  name="Shape Animation",
3973  description="export shape animations - updates the .mesh file",
3974  default=CONFIG['SHAPE_ANIM'])
3975  EX_TRIM_BONE_WEIGHTS = FloatProperty(
3976  name="Trim Weights",
3977  description="ignore bone weights below this value (Ogre supports 4 bones per vertex)",
3978  min=0.0, max=0.5, default=CONFIG['TRIM_BONE_WEIGHTS'] )
3979  EX_ARRAY = BoolProperty(
3980  name="Optimize Arrays",
3981  description="optimize array modifiers as instances (constant offset only)",
3982  default=CONFIG['ARRAY'])
3983  EX_MATERIALS = BoolProperty(
3984  name="Export Materials",
3985  description="exports .material script",
3986  default=CONFIG['MATERIALS'])
3987  EX_FORCE_IMAGE_FORMAT = EnumProperty(
3988  items=_IMAGE_FORMATS,
3989  name='Convert Images',
3990  description='convert all textures to format',
3991  default=CONFIG['FORCE_IMAGE_FORMAT'] )
3992  EX_DDS_MIPS = IntProperty(
3993  name="DDS Mips",
3994  description="number of mip maps (DDS)",
3995  min=0, max=16,
3996  default=CONFIG['DDS_MIPS'])
3997 
3998  # Mesh options
3999  EX_lodLevels = IntProperty(
4000  name="LOD Levels",
4001  description="MESH number of LOD levels",
4002  min=0, max=32,
4003  default=CONFIG['lodLevels'])
4004  EX_lodDistance = IntProperty(
4005  name="LOD Distance",
4006  description="MESH distance increment to reduce LOD",
4007  min=0, max=2000,
4008  default=CONFIG['lodDistance'])
4009  EX_lodPercent = IntProperty(
4010  name="LOD Percentage",
4011  description="LOD percentage reduction",
4012  min=0, max=99,
4013  default=CONFIG['lodPercent'])
4014  EX_nuextremityPoints = IntProperty(
4015  name="Extremity Points",
4016  description="MESH Extremity Points",
4017  min=0, max=65536,
4018  default=CONFIG['nuextremityPoints'])
4019  EX_generateEdgeLists = BoolProperty(
4020  name="Edge Lists",
4021  description="MESH generate edge lists (for stencil shadows)",
4022  default=CONFIG['generateEdgeLists'])
4023  EX_generateTangents = BoolProperty(
4024  name="Tangents",
4025  description="MESH generate tangents",
4026  default=CONFIG['generateTangents'])
4027  EX_tangentSemantic = StringProperty(
4028  name="Tangent Semantic",
4029  description="MESH tangent semantic",
4030  maxlen=16,
4031  default=CONFIG['tangentSemantic'])
4032  EX_tangentUseParity = IntProperty(
4033  name="Tangent Parity",
4034  description="MESH tangent use parity",
4035  min=0, max=16,
4036  default=CONFIG['tangentUseParity'])
4037  EX_tangentSplitMirrored = BoolProperty(
4038  name="Tangent Split Mirrored",
4039  description="MESH split mirrored tangents",
4040  default=CONFIG['tangentSplitMirrored'])
4041  EX_tangentSplitRotated = BoolProperty(
4042  name="Tangent Split Rotated",
4043  description="MESH split rotated tangents",
4044  default=CONFIG['tangentSplitRotated'])
4045  EX_reorganiseBuffers = BoolProperty(
4046  name="Reorganise Buffers",
4047  description="MESH reorganise vertex buffers",
4048  default=CONFIG['reorganiseBuffers'])
4049  EX_optimiseAnimations = BoolProperty(
4050  name="Optimize Animations",
4051  description="MESH optimize animations",
4052  default=CONFIG['optimiseAnimations'])
4053 
4054  filepath= StringProperty(
4055  name="File Path",
4056  description="Filepath used for exporting Ogre .scene file",
4057  maxlen=1024,
4058  default="",
4059  subtype='FILE_PATH')
4060 
4061  EXPORT_TYPE = 'OGRE'
4062 
4063 ## UI panel Tundra export - Subclasses _OgreCommonExport_
4064 
4065 class INFO_OT_createRealxtendExport( bpy.types.Operator, _OgreCommonExport_):
4066  '''Export RealXtend Scene'''
4067  bl_idname = "ogre.export_realxtend"
4068  bl_label = "Export RealXtend"
4069  bl_options = {'REGISTER', 'UNDO'}
4070 
4071  EX_SWAP_AXIS = EnumProperty(
4072  items=AXIS_MODES,
4073  name='swap axis',
4074  description='axis swapping mode',
4075  default= CONFIG['SWAP_AXIS']
4076  )
4077 
4078  # Basic options
4079  EX_SEP_MATS = BoolProperty(
4080  name="Separate Materials",
4081  description="exports a .material for each material (rather than putting all materials in a single .material file)",
4082  default=CONFIG['SEP_MATS'])
4083  EX_ONLY_DEFORMABLE_BONES = BoolProperty(
4084  name="Only Deformable Bones",
4085  description="only exports bones that are deformable. Useful for hiding IK-Bones used in Blender. Note: Any bone with deformable children/descendants will be output as well.",
4086  default=CONFIG['ONLY_DEFORMABLE_BONES'])
4087  EX_ONLY_KEYFRAMED_BONES = BoolProperty(
4088  name="Only Keyframed Bones",
4089  description="only exports bones that have been keyframed for a given animation. Useful to limit the set of bones on a per-animation basis.",
4090  default=CONFIG['ONLY_KEYFRAMED_BONES'])
4091  EX_OGRE_INHERIT_SCALE = BoolProperty(
4092  name="OGRE inherit scale",
4093  description="whether the OGRE bones have the 'inherit scale' flag on. If the animation has scale in it, the exported animation needs to be adjusted to account for the state of the inherit-scale flag in OGRE.",
4094  default=CONFIG['OGRE_INHERIT_SCALE'])
4095  EX_SCENE = BoolProperty(
4096  name="Export Scene",
4097  description="export current scene (OgreDotScene xml)",
4098  default=CONFIG['SCENE'])
4099  EX_SELONLY = BoolProperty(
4100  name="Export Selected Only",
4101  description="export selected",
4102  default=CONFIG['SELONLY'])
4103  EX_EXPORT_HIDDEN = BoolProperty(
4104  name="Export Hidden Also",
4105  description="Export hidden meshes in addition to visible ones. Turn off to avoid exporting hidden stuff.",
4106  default=CONFIG['EXPORT_HIDDEN'])
4107  EX_FORCE_CAMERA = BoolProperty(
4108  name="Force Camera",
4109  description="export active camera",
4110  default=CONFIG['FORCE_CAMERA'])
4111  EX_FORCE_LAMPS = BoolProperty(
4112  name="Force Lamps",
4113  description="export all lamps",
4114  default=CONFIG['FORCE_LAMPS'])
4115  EX_MESH = BoolProperty(
4116  name="Export Meshes",
4117  description="export meshes",
4118  default=CONFIG['MESH'])
4119  EX_MESH_OVERWRITE = BoolProperty(
4120  name="Export Meshes (overwrite)",
4121  description="export meshes (overwrite existing files)",
4122  default=CONFIG['MESH_OVERWRITE'])
4123  EX_ARM_ANIM = BoolProperty(
4124  name="Armature Animation",
4125  description="export armature animations - updates the .skeleton file",
4126  default=CONFIG['ARM_ANIM'])
4127  EX_SHAPE_ANIM = BoolProperty(
4128  name="Shape Animation",
4129  description="export shape animations - updates the .mesh file",
4130  default=CONFIG['SHAPE_ANIM'])
4131  EX_TRIM_BONE_WEIGHTS = FloatProperty(
4132  name="Trim Weights",
4133  description="ignore bone weights below this value (Ogre supports 4 bones per vertex)",
4134  min=0.0, max=0.5, default=CONFIG['TRIM_BONE_WEIGHTS'] )
4135  EX_ARRAY = BoolProperty(
4136  name="Optimize Arrays",
4137  description="optimize array modifiers as instances (constant offset only)",
4138  default=CONFIG['ARRAY'])
4139  EX_MATERIALS = BoolProperty(
4140  name="Export Materials",
4141  description="exports .material script",
4142  default=CONFIG['MATERIALS'])
4143  EX_FORCE_IMAGE_FORMAT = EnumProperty(
4144  name='Convert Images',
4145  description='convert all textures to format',
4146  items=_IMAGE_FORMATS,
4147  default=CONFIG['FORCE_IMAGE_FORMAT'])
4148  EX_DDS_MIPS = IntProperty(
4149  name="DDS Mips",
4150  description="number of mip maps (DDS)",
4151  min=0, max=16,
4152  default=CONFIG['DDS_MIPS'])
4153 
4154  # Mesh options
4155  EX_lodLevels = IntProperty(
4156  name="LOD Levels",
4157  description="MESH number of LOD levels",
4158  min=0, max=32,
4159  default=CONFIG['lodLevels'])
4160  EX_lodDistance = IntProperty(
4161  name="LOD Distance",
4162  description="MESH distance increment to reduce LOD",
4163  min=0, max=2000,
4164  default=CONFIG['lodDistance'])
4165  EX_lodPercent = IntProperty(
4166  name="LOD Percentage",
4167  description="LOD percentage reduction",
4168  min=0, max=99,
4169  default=CONFIG['lodPercent'])
4170  EX_nuextremityPoints = IntProperty(
4171  name="Extremity Points",
4172  description="MESH Extremity Points",
4173  min=0, max=65536,
4174  default=CONFIG['nuextremityPoints'])
4175  EX_generateEdgeLists = BoolProperty(
4176  name="Edge Lists",
4177  description="MESH generate edge lists (for stencil shadows)",
4178  default=CONFIG['generateEdgeLists'])
4179  EX_generateTangents = BoolProperty(
4180  name="Tangents",
4181  description="MESH generate tangents",
4182  default=CONFIG['generateTangents'])
4183  EX_tangentSemantic = StringProperty(
4184  name="Tangent Semantic",
4185  description="MESH tangent semantic",
4186  maxlen=3,
4187  default=CONFIG['tangentSemantic'])
4188  EX_tangentUseParity = IntProperty(
4189  name="Tangent Parity",
4190  description="MESH tangent use parity",
4191  min=0, max=16,
4192  default=CONFIG['tangentUseParity'])
4193  EX_tangentSplitMirrored = BoolProperty(
4194  name="Tangent Split Mirrored",
4195  description="MESH split mirrored tangents",
4196  default=CONFIG['tangentSplitMirrored'])
4197  EX_tangentSplitRotated = BoolProperty(
4198  name="Tangent Split Rotated",
4199  description="MESH split rotated tangents",
4200  default=CONFIG['tangentSplitRotated'])
4201  EX_reorganiseBuffers = BoolProperty(
4202  name="Reorganise Buffers",
4203  description="MESH reorganise vertex buffers",
4204  default=CONFIG['reorganiseBuffers'])
4205  EX_optimiseAnimations = BoolProperty(
4206  name="Optimize Animations",
4207  description="MESH optimize animations",
4208  default=CONFIG['optimiseAnimations'])
4209 
4210  filepath = StringProperty(
4211  name="File Path",
4212  description="Filepath used for exporting .txml file",
4213  maxlen=1024,
4214  default="",
4215  subtype='FILE_PATH')
4216 
4217  EXPORT_TYPE = 'REX'
4218 
4219 ## Ogre node helper
4220 
4221 def _ogre_node_helper( doc, ob, objects, prefix='', pos=None, rot=None, scl=None ):
4222  # shouldn't this be matrix_local?
4223  mat = get_parent_matrix(ob, objects).inverted() * ob.matrix_world
4224 
4225  o = doc.createElement('node')
4226  o.setAttribute('name',prefix+ob.name)
4227  p = doc.createElement('position')
4228  q = doc.createElement('rotation') #('quaternion')
4229  s = doc.createElement('scale')
4230  for n in (p,q,s):
4231  o.appendChild(n)
4232 
4233  if pos:
4234  v = swap(pos)
4235  else:
4236  v = swap( mat.to_translation() )
4237  p.setAttribute('x', '%6f'%v.x)
4238  p.setAttribute('y', '%6f'%v.y)
4239  p.setAttribute('z', '%6f'%v.z)
4240 
4241  if rot:
4242  v = swap(rot)
4243  else:
4244  v = swap( mat.to_quaternion() )
4245  q.setAttribute('qx', '%6f'%v.x)
4246  q.setAttribute('qy', '%6f'%v.y)
4247  q.setAttribute('qz', '%6f'%v.z)
4248  q.setAttribute('qw','%6f'%v.w)
4249 
4250  if scl: # this should not be used
4251  v = swap(scl)
4252  x=abs(v.x); y=abs(v.y); z=abs(v.z)
4253  s.setAttribute('x', '%6f'%x)
4254  s.setAttribute('y', '%6f'%y)
4255  s.setAttribute('z', '%6f'%z)
4256  else: # scale is different in Ogre from blender - rotation is removed
4257  ri = mat.to_quaternion().inverted().to_matrix()
4258  scale = ri.to_4x4() * mat
4259  v = swap( scale.to_scale() )
4260  x=abs(v.x); y=abs(v.y); z=abs(v.z)
4261  s.setAttribute('x', '%6f'%x)
4262  s.setAttribute('y', '%6f'%y)
4263  s.setAttribute('z', '%6f'%z)
4264  return o
4265 
4266 ## MeshMagick
4267 
4268 class MeshMagick(object):
4269  ''' Usage: MeshMagick [global_options] toolname [tool_options] infile(s) -- [outfile(s)]
4270  Available Tools
4271  ===============
4272  info - print information about the mesh.
4273  meshmerge - Merge multiple submeshes into a single mesh.
4274  optimise - Optimise meshes and skeletons.
4275  rename - Rename different elements of meshes and skeletons.
4276  transform - Scale, rotate or otherwise transform a mesh.
4277  '''
4278 
4279  @staticmethod
4280  def get_merge_group( ob ):
4281  return get_merge_group( ob, prefix='magicmerge' )
4282 
4283  @staticmethod
4284  def merge( group, path='/tmp', force_name=None ):
4285  print('-'*80)
4286  print(' mesh magick - merge ')
4287  exe = CONFIG['OGRETOOLS_MESH_MAGICK']
4288  if not os.path.isfile( exe ):
4289  print( 'ERROR: can not find MeshMagick.exe' )
4290  print( exe )
4291  return
4292 
4293  files = []
4294  for ob in group.objects:
4295  if ob.data.users == 1: # single users only
4296  files.append( os.path.join( path, ob.data.name+'.mesh' ) )
4297  print( files[-1] )
4298 
4299  opts = 'meshmerge'
4300  if sys.platform == 'linux2': cmd = '/usr/bin/wine %s %s' %(exe, opts)
4301  else: cmd = '%s %s' %(exe, opts)
4302  if force_name: output = force_name + '.mesh'
4303  else: output = '_%s_.mesh' %group.name
4304  cmd = cmd.split() + files + ['--', os.path.join(path,output) ]
4305  subprocess.call( cmd )
4306  print(' mesh magick - complete ')
4307  print('-'*80)
4308 
4309 ## Ogre Command Line Tools Documentation
4310 
4311 _ogre_command_line_tools_doc = '''
4312 Usage: OgreXMLConverter [options] sourcefile [destfile]
4313 
4314 Available options:
4315 -i = interactive mode - prompt for options
4316 (The next 4 options are only applicable when converting XML to Mesh)
4317 -l lodlevels = number of LOD levels
4318 -v lodvalue = value increment to reduce LOD
4319 -s lodstrategy = LOD strategy to use for this mesh
4320 -p lodpercent = Percentage triangle reduction amount per LOD
4321 -f lodnumtris = Fixed vertex reduction per LOD
4322 -e = DON'T generate edge lists (for stencil shadows)
4323 -r = DON'T reorganise vertex buffers to OGRE recommended format.
4324 -t = Generate tangents (for normal mapping)
4325 -td [uvw|tangent]
4326  = Tangent vertex semantic destination (default tangent)
4327 -ts [3|4] = Tangent size (3 or 4 components, 4 includes parity, default 3)
4328 -tm = Split tangent vertices at UV mirror points
4329 -tr = Split tangent vertices where basis is rotated > 90 degrees
4330 -o = DON'T optimise out redundant tracks & keyframes
4331 -d3d = Prefer D3D packed colour formats (default on Windows)
4332 -gl = Prefer GL packed colour formats (default on non-Windows)
4333 -E endian = Set endian mode 'big' 'little' or 'native' (default)
4334 -x num = Generate no more than num eXtremes for every submesh (default 0)
4335 -q = Quiet mode, less output
4336 -log filename = name of the log file (default: 'OgreXMLConverter.log')
4337 sourcefile = name of file to convert
4338 destfile = optional name of file to write to. If you don't
4339  specify this OGRE works it out through the extension
4340  and the XML contents if the source is XML. For example
4341  test.mesh becomes test.xml, test.xml becomes test.mesh
4342  if the XML document root is <mesh> etc.
4343 '''
4344 
4345 ## Ogre Command Line Tools
4346 
4347 def OgreXMLConverter( infile, has_uvs=False ):
4348  # todo: Show a UI dialog to show this error. It's pretty fatal for normal usage.
4349  # We should show how to configure the converter location in config panel or tell the default path.
4350  exe = CONFIG['OGRETOOLS_XML_CONVERTER']
4351  if not os.path.isfile( exe ):
4352  print( 'WARNING: can not find OgreXMLConverter (can not convert XXX.mesh.xml to XXX.mesh' )
4353  return
4354 
4355  basicArguments = ''
4356 
4357  # LOD generation with OgreXMLConverter tool does not work. Currently the mesh files are generated
4358  # manually and referenced in the main mesh file.
4359  #if CONFIG['lodLevels']:
4360  # basicArguments += ' -l %s -v %s -p %s' %(CONFIG['lodLevels'], CONFIG['lodDistance'], CONFIG['lodPercent'])
4361 
4362  if CONFIG['nuextremityPoints'] > 0:
4363  basicArguments += ' -x %s' %CONFIG['nuextremityPoints']
4364 
4365  if not CONFIG['generateEdgeLists']:
4366  basicArguments += ' -e'
4367 
4368  # note: OgreXmlConverter fails to convert meshes without UVs
4369  if CONFIG['generateTangents'] and has_uvs:
4370  basicArguments += ' -t'
4371  if CONFIG['tangentSemantic']:
4372  basicArguments += ' -td %s' %CONFIG['tangentSemantic']
4373  if CONFIG['tangentUseParity']:
4374  basicArguments += ' -ts %s' %CONFIG['tangentUseParity']
4375  if CONFIG['tangentSplitMirrored']:
4376  basicArguments += ' -tm'
4377  if CONFIG['tangentSplitRotated']:
4378  basicArguments += ' -tr'
4379  if not CONFIG['reorganiseBuffers']:
4380  basicArguments += ' -r'
4381  if not CONFIG['optimiseAnimations']:
4382  basicArguments += ' -o'
4383 
4384  # Make xml converter print less stuff, comment this if you want more debug info out
4385  basicArguments += ' -q'
4386 
4387  opts = '-log _ogre_debug.txt %s' %basicArguments
4388  path,name = os.path.split( infile )
4389 
4390  cmd = '%s %s' %(exe, opts)
4391  cmd = cmd.split() + [infile]
4392  subprocess.call( cmd )
4393 
4394 ## Bone
4395 
4396 class Bone(object):
4397 
4398  def __init__(self, rbone, pbone, skeleton):
4399  if CONFIG['SWAP_AXIS'] == 'xyz':
4400  self.fixUpAxis = False
4401  else:
4402  self.fixUpAxis = True
4403  if CONFIG['SWAP_AXIS'] == '-xzy': # Tundra 1.x
4404  self.flipMat = mathutils.Matrix(((-1,0,0,0),(0,0,1,0),(0,1,0,0),(0,0,0,1)))
4405  elif CONFIG['SWAP_AXIS'] == 'xz-y': # Tundra 2.x current generation
4406  #self.flipMat = mathutils.Matrix(((1,0,0,0),(0,0,1,0),(0,1,0,0),(0,0,0,1)))
4407  self.flipMat = mathutils.Matrix(((1,0,0,0),(0,0,1,0),(0,-1,0,0),(0,0,0,1))) # thanks to Waruck
4408  else:
4409  print( 'ERROR - TODO: axis swap mode not supported with armature animation' )
4410  assert 0
4411 
4412  self.skeleton = skeleton
4413  self.name = pbone.name
4414  self.matrix = rbone.matrix_local.copy() # armature space
4415  #self.matrix_local = rbone.matrix.copy() # space?
4416 
4417  self.bone = pbone # safe to hold pointer to pose bone, not edit bone!
4418  self.shouldOutput = True
4419  if CONFIG['ONLY_DEFORMABLE_BONES'] and not pbone.bone.use_deform:
4420  self.shouldOutput = False
4421 
4422  # todo: Test -> #if pbone.bone.use_inherit_scale: print('warning: bone <%s> is using inherit scaling, Ogre has no support for this' %self.name)
4423  self.parent = pbone.parent
4424  self.children = []
4425 
4426  def update(self): # called on frame update
4427  pbone = self.bone
4428  pose = pbone.matrix.copy()
4429  self._inverse_total_trans_pose = pose.inverted()
4430  # calculate difference to parent bone
4431  if self.parent:
4432  pose = self.parent._inverse_total_trans_pose* pose
4433  elif self.fixUpAxis:
4434  pose = self.flipMat * pose
4435  else:
4436  pass
4437 
4438  self.pose_location = pose.to_translation() - self.ogre_rest_matrix.to_translation()
4439  pose = self.inverse_ogre_rest_matrix * pose
4440  self.pose_rotation = pose.to_quaternion()
4441 
4442  #self.pose_location = pbone.location.copy()
4443  #self.pose_scale = pbone.scale.copy()
4444  #if pbone.rotation_mode == 'QUATERNION':
4445  # self.pose_rotation = pbone.rotation_quaternion.copy()
4446  #else:
4447  # self.pose_rotation = pbone.rotation_euler.to_quaternion()
4448 
4449  if CONFIG['OGRE_INHERIT_SCALE']:
4450  # special case workaround for broken Ogre nonuniform scaling:
4451  # Ogre can't deal with arbitrary nonuniform scaling, but it can handle certain special cases
4452  # The special case we are trying to handle here is when a bone has a nonuniform scale and it's
4453  # child bones are not inheriting the scale. We should be able to do this without having to
4454  # do any extra setup in Ogre (like turning off "inherit scale" on the Ogre bones)
4455  # if Ogre is inheriting scale, we just output the scale relative to the parent
4456  self.pose_scale = pose.to_scale()
4457  self.ogreDerivedScale = self.pose_scale.copy()
4458  if self.parent:
4459  # this is how Ogre handles inheritance of scale
4460  self.ogreDerivedScale[0] *= self.parent.ogreDerivedScale[0]
4461  self.ogreDerivedScale[1] *= self.parent.ogreDerivedScale[1]
4462  self.ogreDerivedScale[2] *= self.parent.ogreDerivedScale[2]
4463  # if we don't want inherited scale,
4464  if not self.bone.bone.use_inherit_scale:
4465  # cancel out the scale that Ogre will calculate
4466  scl = self.parent.ogreDerivedScale
4467  self.pose_scale = mathutils.Vector((1.0/scl[0], 1.0/scl[1], 1.0/scl[2]))
4468  self.ogreDerivedScale = mathutils.Vector((1.0, 1.0, 1.0))
4469  else:
4470  # if Ogre is not inheriting the scale,
4471  # just output the scale directly
4472  self.pose_scale = pbone.scale.copy()
4473  # however, if Blender is inheriting the scale,
4474  if self.parent and self.bone.bone.use_inherit_scale:
4475  # apply parent's scale (only works for uniform scaling)
4476  self.pose_scale[0] *= self.parent.pose_scale[0]
4477  self.pose_scale[1] *= self.parent.pose_scale[1]
4478  self.pose_scale[2] *= self.parent.pose_scale[2]
4479 
4480  for child in self.children:
4481  child.update()
4482 
4483  def clear_pose_transform( self ):
4484  self.bone.location.zero()
4485  self.bone.scale.Fill(3, 1.0)
4486  self.bone.rotation_quaternion.identity()
4487  self.bone.rotation_euler.zero()
4488  #self.bone.rotation_axis_angle #ignore axis angle mode
4489 
4490  def save_pose_transform( self ):
4491  self.savedPoseLocation = self.bone.location.copy()
4492  self.savedPoseScale = self.bone.scale.copy()
4493  self.savedPoseRotationQ = self.bone.rotation_quaternion
4494  self.savedPoseRotationE = self.bone.rotation_euler
4495  #self.bone.rotation_axis_angle #ignore axis angle mode
4496 
4497  def restore_pose_transform( self ):
4498  self.bone.location = self.savedPoseLocation
4499  self.bone.scale = self.savedPoseScale
4500  self.bone.rotation_quaternion = self.savedPoseRotationQ
4501  self.bone.rotation_euler = self.savedPoseRotationE
4502  #self.bone.rotation_axis_angle #ignore axis angle mode
4503 
4504  def rebuild_tree( self ): # called first on all bones
4505  if self.parent:
4506  self.parent = self.skeleton.get_bone( self.parent.name )
4507  self.parent.children.append( self )
4508  if self.shouldOutput and not self.parent.shouldOutput:
4509  # mark all ancestor bones as shouldOutput
4510  parent = self.parent
4511  while parent:
4512  parent.shouldOutput = True
4513  parent = parent.parent
4514 
4515  def compute_rest( self ): # called after rebuild_tree, recursive roots to leaves
4516  if self.parent:
4517  inverseParentMatrix = self.parent.inverse_total_trans
4518  elif self.fixUpAxis:
4519  inverseParentMatrix = self.flipMat
4520  else:
4521  inverseParentMatrix = mathutils.Matrix(((1,0,0,0),(0,1,0,0),(0,0,1,0),(0,0,0,1)))
4522 
4523  #self.ogre_rest_matrix = self.skeleton.object_space_transformation * self.matrix # ALLOW ROTATION?
4524  self.ogre_rest_matrix = self.matrix.copy()
4525  # store total inverse transformation
4526  self.inverse_total_trans = self.ogre_rest_matrix.inverted()
4527  # relative to OGRE parent bone origin
4528  self.ogre_rest_matrix = inverseParentMatrix * self.ogre_rest_matrix
4529  self.inverse_ogre_rest_matrix = self.ogre_rest_matrix.inverted()
4530 
4531  # recursion
4532  for child in self.children:
4533  child.compute_rest()
4534 
4535 class Keyframe:
4536  def __init__(self, time, pos, rot, scale):
4537  self.time = time
4538  self.pos = pos.copy()
4539  self.rot = rot.copy()
4540  self.scale = scale.copy()
4541 
4542  def isTransIdentity( self ):
4543  return self.pos.length < 0.0001
4544 
4545  def isRotIdentity( self ):
4546  # if the angle is very close to zero, or the axis is not unit length,
4547  if abs(self.rot.angle) < 0.0001 or abs(self.rot.axis.length - 1.0) > 0.001:
4548  # treat it as a zero rotation
4549  return True
4550  return False
4551 
4552  def isScaleIdentity( self ):
4553  scaleDiff = mathutils.Vector((1,1,1)) - self.scale
4554  return scaleDiff.length < 0.0001
4555 
4556 
4557 # Bone_Track
4558 # Encapsulates all of the key information for an individual bone within a single animation,
4559 # and srores that information as XML.
4560 class Bone_Track:
4561  def __init__(self, bone):
4562  self.bone = bone
4563  self.keyframes = []
4564 
4565  def is_pos_animated( self ):
4566  # take note if any keyframe is anything other than the IDENTITY transform
4567  for kf in self.keyframes:
4568  if not kf.isTransIdentity():
4569  return True
4570  return False
4571 
4572  def is_rot_animated( self ):
4573  # take note if any keyframe is anything other than the IDENTITY transform
4574  for kf in self.keyframes:
4575  if not kf.isRotIdentity():
4576  return True
4577  return False
4578 
4579  def is_scale_animated( self ):
4580  # take note if any keyframe is anything other than the IDENTITY transform
4581  for kf in self.keyframes:
4582  if not kf.isScaleIdentity():
4583  return True
4584  return False
4585 
4586  def add_keyframe( self, time ):
4587  bone = self.bone
4588  kf = Keyframe(time, bone.pose_location, bone.pose_rotation, bone.pose_scale)
4589  self.keyframes.append( kf )
4590 
4591  def write_track( self, doc, tracks_element ):
4592  isPosAnimated = self.is_pos_animated()
4593  isRotAnimated = self.is_rot_animated()
4594  isScaleAnimated = self.is_scale_animated()
4595  if not isPosAnimated and not isRotAnimated and not isScaleAnimated:
4596  return
4597  track = doc.createElement('track')
4598  track.setAttribute('bone', self.bone.name)
4599  keyframes_element = doc.createElement('keyframes')
4600  track.appendChild( keyframes_element )
4601  for kf in self.keyframes:
4602  keyframe = doc.createElement('keyframe')
4603  keyframe.setAttribute('time', '%6f' % kf.time)
4604  if isPosAnimated:
4605  trans = doc.createElement('translate')
4606  keyframe.appendChild( trans )
4607  trans.setAttribute('x', '%6f' % kf.pos.x)
4608  trans.setAttribute('y', '%6f' % kf.pos.y)
4609  trans.setAttribute('z', '%6f' % kf.pos.z)
4610 
4611  if isRotAnimated:
4612  rotElement = doc.createElement( 'rotate' )
4613  keyframe.appendChild( rotElement )
4614  angle = kf.rot.angle
4615  axis = kf.rot.axis
4616  # if angle is near zero or axis is not unit magnitude,
4617  if kf.isRotIdentity():
4618  angle = 0.0 # avoid outputs like "-0.00000"
4619  axis = mathutils.Vector((0,0,0))
4620  rotElement.setAttribute('angle', '%6f' %angle )
4621  axisElement = doc.createElement('axis')
4622  rotElement.appendChild( axisElement )
4623  axisElement.setAttribute('x', '%6f' %axis[0])
4624  axisElement.setAttribute('y', '%6f' %axis[1])
4625  axisElement.setAttribute('z', '%6f' %axis[2])
4626 
4627  if isScaleAnimated:
4628  scale = doc.createElement('scale')
4629  keyframe.appendChild( scale )
4630  x,y,z = kf.scale
4631  scale.setAttribute('x', '%6f' %x)
4632  scale.setAttribute('y', '%6f' %y)
4633  scale.setAttribute('z', '%6f' %z)
4634  keyframes_element.appendChild( keyframe )
4635  tracks_element.appendChild( track )
4636 
4637 # Skeleton
4638 def findArmature( ob ):
4639  arm = ob.find_armature()
4640  # if this armature has no animation,
4641  if not arm.animation_data:
4642  # search for another armature that is a proxy for it
4643  for ob2 in bpy.data.objects:
4644  if ob2.type == 'ARMATURE' and ob2.proxy == arm:
4645  print( "proxy armature %s found" % ob2.name )
4646  return ob2
4647  return arm
4648 
4649 class Skeleton(object):
4650  def get_bone( self, name ):
4651  for b in self.bones:
4652  if b.name == name:
4653  return b
4654  return None
4655 
4656  def __init__(self, ob ):
4657  if ob.location.x != 0 or ob.location.y != 0 or ob.location.z != 0:
4658  Report.warnings.append('ERROR: Mesh (%s): is offset from Armature - zero transform is required' %ob.name)
4659  if ob.scale.x != 1 or ob.scale.y != 1 or ob.scale.z != 1:
4660  Report.warnings.append('ERROR: Mesh (%s): has been scaled - scale(1,1,1) is required' %ob.name)
4661 
4662  self.object = ob
4663  self.bones = []
4664  mats = {}
4665  self.arm = arm = findArmature( ob )
4666  arm.hide = False
4667  self._restore_layers = list(arm.layers)
4668  #arm.layers = [True]*20 # can not have anything hidden - REQUIRED?
4669 
4670  for pbone in arm.pose.bones:
4671  mybone = Bone( arm.data.bones[pbone.name], pbone, self )
4672  self.bones.append( mybone )
4673 
4674  if arm.name not in Report.armatures:
4675  Report.armatures.append( arm.name )
4676 
4677  ## bad idea - allowing rotation of armature, means vertices must also be rotated,
4678  ## also a bug with applying the rotation, the Z rotation is lost
4679  #x,y,z = arm.matrix_local.copy().inverted().to_euler()
4680  #e = mathutils.Euler( (x,z,y) )
4681  #self.object_space_transformation = e.to_matrix().to_4x4()
4682  x,y,z = arm.matrix_local.to_euler()
4683  if x != 0 or y != 0 or z != 0:
4684  Report.warnings.append('ERROR: Armature: %s is rotated - (rotation is ignored)' %arm.name)
4685 
4686  ## setup bones for Ogre format ##
4687  for b in self.bones:
4688  b.rebuild_tree()
4689  ## walk bones, convert them ##
4690  self.roots = []
4691  ep = 0.0001
4692  for b in self.bones:
4693  if not b.parent:
4694  b.compute_rest()
4695  loc,rot,scl = b.ogre_rest_matrix.decompose()
4696  #if loc.x or loc.y or loc.z:
4697  # Report.warnings.append('ERROR: root bone has non-zero transform (location offset)')
4698  #if rot.w > ep or rot.x > ep or rot.y > ep or rot.z < 1.0-ep:
4699  # Report.warnings.append('ERROR: root bone has non-zero transform (rotation offset)')
4700  self.roots.append( b )
4701 
4702  def write_animation( self, arm, actionName, frameBegin, frameEnd, doc, parentElement ):
4703  _fps = float( bpy.context.scene.render.fps )
4704  #boneNames = sorted( [bone.name for bone in arm.pose.bones] )
4705  bone_tracks = []
4706  for bone in self.bones:
4707  #bone = self.get_bone(boneName)
4708  if bone.shouldOutput:
4709  bone_tracks.append( Bone_Track(bone) )
4710  bone.clear_pose_transform() # clear out any leftover pose transforms in case this bone isn't keyframed
4711  for frame in range( int(frameBegin), int(frameEnd)+1, bpy.context.scene.frame_step):#thanks to Vesa
4712  bpy.context.scene.frame_set(frame)
4713  for bone in self.roots:
4714  bone.update()
4715  for track in bone_tracks:
4716  track.add_keyframe((frame - frameBegin) / _fps)
4717  # check to see if any animation tracks would be output
4718  animationFound = False
4719  for track in bone_tracks:
4720  if track.is_pos_animated() or track.is_rot_animated() or track.is_scale_animated():
4721  animationFound = True
4722  break
4723  if not animationFound:
4724  return
4725  anim = doc.createElement('animation')
4726  parentElement.appendChild( anim )
4727  tracks = doc.createElement('tracks')
4728  anim.appendChild( tracks )
4729  Report.armature_animations.append( '%s : %s [start frame=%s end frame=%s]' %(arm.name, actionName, frameBegin, frameEnd) )
4730 
4731  anim.setAttribute('name', actionName) # USE the action name
4732  anim.setAttribute('length', '%6f' %( (frameEnd - frameBegin)/_fps ) )
4733 
4734  for track in bone_tracks:
4735  # will only write a track if there is some kind of animation there
4736  track.write_track( doc, tracks )
4737 
4738  def to_xml( self ):
4739  doc = RDocument()
4740  root = doc.createElement('skeleton'); doc.appendChild( root )
4741  bones = doc.createElement('bones'); root.appendChild( bones )
4742  bh = doc.createElement('bonehierarchy'); root.appendChild( bh )
4743  boneId = 0
4744  for bone in self.bones:
4745  if not bone.shouldOutput:
4746  continue
4747  b = doc.createElement('bone')
4748  b.setAttribute('name', bone.name)
4749  b.setAttribute('id', str(boneId) )
4750  boneId = boneId + 1
4751  bones.appendChild( b )
4752  mat = bone.ogre_rest_matrix.copy()
4753  if bone.parent:
4754  bp = doc.createElement('boneparent')
4755  bp.setAttribute('bone', bone.name)
4756  bp.setAttribute('parent', bone.parent.name)
4757  bh.appendChild( bp )
4758 
4759  pos = doc.createElement( 'position' ); b.appendChild( pos )
4760  x,y,z = mat.to_translation()
4761  pos.setAttribute('x', '%6f' %x )
4762  pos.setAttribute('y', '%6f' %y )
4763  pos.setAttribute('z', '%6f' %z )
4764  rot = doc.createElement( 'rotation' ) # "rotation", not "rotate"
4765  b.appendChild( rot )
4766 
4767  q = mat.to_quaternion()
4768  rot.setAttribute('angle', '%6f' %q.angle )
4769  axis = doc.createElement('axis'); rot.appendChild( axis )
4770  x,y,z = q.axis
4771  axis.setAttribute('x', '%6f' %x )
4772  axis.setAttribute('y', '%6f' %y )
4773  axis.setAttribute('z', '%6f' %z )
4774 
4775  # Ogre bones do not have initial scaling
4776 
4777  arm = self.arm
4778  # remember some things so we can put them back later
4779  savedFrame = bpy.context.scene.frame_current
4780  # save the current pose
4781  for b in self.bones:
4782  b.save_pose_transform()
4783 
4784  anims = doc.createElement('animations')
4785  root.appendChild( anims )
4786  if not arm.animation_data or (arm.animation_data and not arm.animation_data.nla_tracks):
4787  # write a single animation from the blender timeline
4788  self.write_animation( arm, 'my_animation', bpy.context.scene.frame_start, bpy.context.scene.frame_end, doc, anims )
4789 
4790  elif arm.animation_data:
4791  savedUseNla = arm.animation_data.use_nla
4792  savedAction = arm.animation_data.action
4793  arm.animation_data.use_nla = False
4794  if not len( arm.animation_data.nla_tracks ):
4795  Report.warnings.append('you must assign an NLA strip to armature (%s) that defines the start and end frames' %arm.name)
4796 
4797  actions = {} # actions by name
4798  # the only thing NLA is used for is to gather the names of the actions
4799  # it doesn't matter if the actions are all in the same NLA track or in different tracks
4800  for nla in arm.animation_data.nla_tracks: # NLA required, lone actions not supported
4801  print('NLA track:', nla.name)
4802 
4803  for strip in nla.strips:
4804  action = strip.action
4805  actions[ action.name ] = action
4806  print(' strip name:', strip.name)
4807  print(' action name:', action.name)
4808 
4809  actionNames = sorted( actions.keys() ) # output actions in alphabetical order
4810  for actionName in actionNames:
4811  action = actions[ actionName ]
4812  arm.animation_data.action = action # set as the current action
4813  suppressedBones = []
4814  if CONFIG['ONLY_KEYFRAMED_BONES']:
4815  keyframedBones = {}
4816  for group in action.groups:
4817  keyframedBones[ group.name ] = True
4818  for b in self.bones:
4819  if (not b.name in keyframedBones) and b.shouldOutput:
4820  # suppress this bone's output
4821  b.shouldOutput = False
4822  suppressedBones.append( b.name )
4823  self.write_animation( arm, actionName, action.frame_range[0], action.frame_range[1], doc, anims )
4824  # restore suppressed bones
4825  for boneName in suppressedBones:
4826  bone = self.get_bone( boneName )
4827  bone.shouldOutput = True
4828  # restore these to what they originally were
4829  arm.animation_data.action = savedAction
4830  arm.animation_data.use_nla = savedUseNla
4831 
4832  # restore
4833  bpy.context.scene.frame_set( savedFrame )
4834  # restore the current pose
4835  for b in self.bones:
4836  b.restore_pose_transform()
4837 
4838  return doc.toprettyxml()
4839 
4840 
4841 ## Selector extras
4842 
4843 class INFO_MT_instances(bpy.types.Menu):
4844  bl_label = "Instances"
4845 
4846  def draw(self, context):
4847  layout = self.layout
4848  inst = gather_instances()
4849  for data in inst:
4850  ob = inst[data][0]
4851  op = layout.operator(INFO_MT_instance.bl_idname, text=ob.name) # operator has no variable for button name?
4852  op.mystring = ob.name
4853  layout.separator()
4854 
4855 class INFO_MT_instance(bpy.types.Operator):
4856  '''select instance group'''
4857  bl_idname = "ogre.select_instances"
4858  bl_label = "Select Instance Group"
4859  bl_options = {'REGISTER', 'UNDO'} # Options for this panel type
4860  mystring= StringProperty(name="MyString", description="hidden string", maxlen=1024, default="my string")
4861 
4862  @classmethod
4863  def poll(cls, context):
4864  return True
4865 
4866  def invoke(self, context, event):
4867  print( 'invoke select_instances op', event )
4868  select_instances( context, self.mystring )
4869  return {'FINISHED'}
4870 
4871 class INFO_MT_groups(bpy.types.Menu):
4872  bl_label = "Groups"
4873 
4874  def draw(self, context):
4875  layout = self.layout
4876  for group in bpy.data.groups:
4877  op = layout.operator(INFO_MT_group.bl_idname, text=group.name) # operator no variable for button name?
4878  op.mystring = group.name
4879  layout.separator()
4880 
4881 class INFO_MT_group(bpy.types.Operator):
4882  '''select group'''
4883  bl_idname = "ogre.select_group"
4884  bl_label = "Select Group"
4885  bl_options = {'REGISTER'} # Options for this panel type
4886  mystring= StringProperty(name="MyString", description="hidden string", maxlen=1024, default="my string")
4887 
4888  @classmethod
4889  def poll(cls, context):
4890  return True
4891 
4892  def invoke(self, context, event):
4893  select_group( context, self.mystring )
4894  return {'FINISHED'}
4895 
4896 ## NVIDIA texture tool documentation
4897 
4898 NVDXT_DOC = '''
4899 Version 8.30
4900 NVDXT
4901 This program
4902  compresses images
4903  creates normal maps from color or alpha
4904  creates DuDv map
4905  creates cube maps
4906  writes out .dds file
4907  does batch processing
4908  reads .tga, .bmp, .gif, .ppm, .jpg, .tif, .cel, .dds, .png, .psd, .rgb, *.bw and .rgba
4909  filters MIP maps
4910 
4911 Options:
4912  -profile <profile name> : Read a profile created from the Photoshop plugin
4913  -quick : use fast compression method
4914  -quality_normal : normal quality compression
4915  -quality_production : production quality compression
4916  -quality_highest : highest quality compression (this can be very slow)
4917  -rms_threshold <int> : quality RMS error. Above this, an extensive search is performed.
4918  -prescale <int> <int>: rescale image to this size first
4919  -rescale <nearest | hi | lo | next_lo>: rescale image to nearest, next highest or next lowest power of two
4920  -rel_scale <float, float> : relative scale of original image. 0.5 is half size Default 1.0, 1.0
4921 
4922 Optional Filtering for rescaling. Default cube filter:
4923  -RescalePoint
4924  -RescaleBox
4925  -RescaleTriangle
4926  -RescaleQuadratic
4927  -RescaleCubic
4928  -RescaleCatrom
4929  -RescaleMitchell
4930  -RescaleGaussian
4931  -RescaleSinc
4932  -RescaleBessel
4933  -RescaleHanning
4934  -RescaleHamming
4935  -RescaleBlackman
4936  -RescaleKaiser
4937  -clamp <int, int> : maximum image size. image width and height are clamped
4938  -clampScale <int, int> : maximum image size. image width and height are scaled
4939  -window <left, top, right, bottom> : window of original window to compress
4940  -nomipmap : don't generate MIP maps
4941  -nmips <int> : specify the number of MIP maps to generate
4942  -rgbe : Image is RGBE format
4943  -dither : add dithering
4944  -sharpenMethod <method>: sharpen method MIP maps
4945  <method> is
4946  None
4947  Negative
4948  Lighter
4949  Darker
4950  ContrastMore
4951  ContrastLess
4952  Smoothen
4953  SharpenSoft
4954  SharpenMedium
4955  SharpenStrong
4956  FindEdges
4957  Contour
4958  EdgeDetect
4959  EdgeDetectSoft
4960  Emboss
4961  MeanRemoval
4962  UnSharp <radius, amount, threshold>
4963  XSharpen <xsharpen_strength, xsharpen_threshold>
4964  Custom
4965  -pause : wait for keyboard on error
4966  -flip : flip top to bottom
4967  -timestamp : Update only changed files
4968  -list <filename> : list of files to convert
4969  -cubeMap : create cube map .
4970  Cube faces specified with individual files with -list option
4971  positive x, negative x, positive y, negative y, positive z, negative z
4972  Use -output option to specify filename
4973  Cube faces specified in one file. Use -file to specify input filename
4974 
4975  -volumeMap : create volume texture.
4976  Volume slices specified with individual files with -list option
4977  Use -output option to specify filename
4978  Volume specified in one file. Use -file to specify input filename
4979 
4980  -all : all image files in current directory
4981  -outdir <directory>: output directory
4982  -deep [directory]: include all subdirectories
4983  -outsamedir : output directory same as input
4984  -overwrite : if input is .dds file, overwrite old file
4985  -forcewrite : write over readonly files
4986  -file <filename> : input file to process. Accepts wild cards
4987  -output <filename> : filename to write to [-outfile can also be specified]
4988  -append <filename_append> : append this string to output filename
4989  -8 <dxt1c | dxt1a | dxt3 | dxt5 | u1555 | u4444 | u565 | u8888 | u888 | u555 | L8 | A8> : compress 8 bit images with this format
4990  -16 <dxt1c | dxt1a | dxt3 | dxt5 | u1555 | u4444 | u565 | u8888 | u888 | u555 | A8L8> : compress 16 bit images with this format
4991  -24 <dxt1c | dxt1a | dxt3 | dxt5 | u1555 | u4444 | u565 | u8888 | u888 | u555> : compress 24 bit images with this format
4992  -32 <dxt1c | dxt1a | dxt3 | dxt5 | u1555 | u4444 | u565 | u8888 | u888 | u555> : compress 32 bit images with this format
4993 
4994  -swapRB : swap rb
4995  -swapRG : swap rg
4996  -gamma <float value>: gamma correcting during filtering
4997  -outputScale <float, float, float, float>: scale the output by this (r,g,b,a)
4998  -outputBias <float, float, float, float>: bias the output by this amount (r,g,b,a)
4999  -outputWrap : wraps overflow values modulo the output format
5000  -inputScale <float, float, float, float>: scale the inpput by this (r,g,b,a)
5001  -inputBias <float, float, float, float>: bias the input by this amount (r,g,b,a)
5002  -binaryalpha : treat alpha as 0 or 1
5003  -alpha_threshold <byte>: [0-255] alpha reference value
5004  -alphaborder : border images with alpha = 0
5005  -alphaborderLeft : border images with alpha (left) = 0
5006  -alphaborderRight : border images with alpha (right)= 0
5007  -alphaborderTop : border images with alpha (top) = 0
5008  -alphaborderBottom : border images with alpha (bottom)= 0
5009  -fadeamount <int>: percentage to fade each MIP level. Default 15
5010 
5011  -fadecolor : fade map (color, normal or DuDv) over MIP levels
5012  -fadetocolor <hex color> : color to fade to
5013  -custom_fade <n> <n fadeamounts> : set custom fade amount. n is number number of fade amounts. fadeamount are [0,1]
5014  -fadealpha : fade alpha over MIP levels
5015  -fadetoalpha <byte>: [0-255] alpha to fade to
5016  -border : border images with color
5017  -bordercolor <hex color> : color for border
5018  -force4 : force DXT1c to use always four colors
5019  -weight <float, float, float>: Compression weightings for R G and B
5020  -luminance : convert color values to luminance for L8 formats
5021  -greyScale : Convert to grey scale
5022  -greyScaleWeights <float, float, float, float>: override greyscale conversion weights of (0.3086, 0.6094, 0.0820, 0)
5023  -brightness <float, float, float, float>: per channel brightness. Default 0.0 usual range [0,1]
5024  -contrast <float, float, float, float>: per channel contrast. Default 1.0 usual range [0.5, 1.5]
5025 
5026 Texture Format Default DXT3:
5027  -dxt1c : DXT1 (color only)
5028  -dxt1a : DXT1 (one bit alpha)
5029  -dxt3 : DXT3
5030  -dxt5 : DXT5n
5031  -u1555 : uncompressed 1:5:5:5
5032  -u4444 : uncompressed 4:4:4:4
5033  -u565 : uncompressed 5:6:5
5034  -u8888 : uncompressed 8:8:8:8
5035  -u888 : uncompressed 0:8:8:8
5036  -u555 : uncompressed 0:5:5:5
5037  -p8c : paletted 8 bit (256 colors)
5038  -p8a : paletted 8 bit (256 colors with alpha)
5039  -p4c : paletted 4 bit (16 colors)
5040  -p4a : paletted 4 bit (16 colors with alpha)
5041  -a8 : 8 bit alpha channel
5042  -cxv8u8 : normal map format
5043  -v8u8 : EMBM format (8, bit two component signed)
5044  -v16u16 : EMBM format (16 bit, two component signed)
5045  -A8L8 : 8 bit alpha channel, 8 bit luminance
5046  -fp32x4 : fp32 four channels (A32B32G32R32F)
5047  -fp32 : fp32 one channel (R32F)
5048  -fp16x4 : fp16 four channels (A16B16G16R16F)
5049  -dxt5nm : dxt5 style normal map
5050  -3Dc : 3DC
5051  -g16r16 : 16 bit in, two component
5052  -g16r16f : 16 bit float, two components
5053 
5054 Mip Map Filtering Options. Default box filter:
5055  -Point
5056  -Box
5057  -Triangle
5058  -Quadratic
5059  -Cubic
5060  -Catrom
5061  -Mitchell
5062  -Gaussian
5063  -Sinc
5064  -Bessel
5065  -Hanning
5066  -Hamming
5067  -Blackman
5068  -Kaiser
5069 
5070 ***************************
5071 To make a normal or dudv map, specify one of
5072  -n4 : normal map 4 sample
5073  -n3x3 : normal map 3x3 filter
5074  -n5x5 : normal map 5x5 filter
5075  -n7x7 : normal map 7x7 filter
5076  -n9x9 : normal map 9x9 filter
5077  -dudv : DuDv
5078 
5079 and source of height info:
5080  -alpha : alpha channel
5081  -rgb : average rgb
5082  -biased : average rgb biased
5083  -red : red channel
5084  -green : green channel
5085  -blue : blue channel
5086  -max : max of (r,g,b)
5087  -colorspace : mix of r,g,b
5088 
5089 -norm : normalize mip maps (source is a normal map)
5090 
5091 -toHeight : create a height map (source is a normal map)
5092 
5093 
5094 Normal/DuDv Map dxt:
5095  -aheight : store calculated height in alpha field
5096  -aclear : clear alpha channel
5097  -awhite : set alpha channel = 1.0
5098  -scale <float> : scale of height map. Default 1.0
5099  -wrap : wrap texture around. Default off
5100  -minz <int> : minimum value for up vector [0-255]. Default 0
5101 
5102 ***************************
5103 To make a depth sprite, specify:
5104  -depth
5105 
5106 and source of depth info:
5107  -alpha : alpha channel
5108  -rgb : average rgb (default)
5109  -red : red channel
5110  -green : green channel
5111  -blue : blue channel
5112  -max : max of (r,g,b)
5113  -colorspace : mix of r,g,b
5114 
5115 Depth Sprite dxt:
5116  -aheight : store calculated depth in alpha channel
5117  -aclear : store 0.0 in alpha channel
5118  -awhite : store 1.0 in alpha channel
5119  -scale <float> : scale of depth sprite (default 1.0)
5120  -alpha_modulate : multiplies color by alpha during filtering
5121  -pre_modulate : multiplies color by alpha before processing
5122 
5123 Examples
5124  nvdxt -cubeMap -list cubemapfile.lst -output cubemap.dds
5125  nvdxt -cubeMap -file cubemapfile.tga
5126  nvdxt -file test.tga -dxt1c
5127  nvdxt -file *.tga
5128  nvdxt -file c:\temp\*.tga
5129  nvdxt -file temp\*.tga
5130  nvdxt -file height_field_in_alpha.tga -n3x3 -alpha -scale 10 -wrap
5131  nvdxt -file grey_scale_height_field.tga -n5x5 -rgb -scale 1.3
5132  nvdxt -file normal_map.tga -norm
5133  nvdxt -file image.tga -dudv -fade -fadeamount 10
5134  nvdxt -all -dxt3 -gamma -outdir .\dds_dir -time
5135  nvdxt -file *.tga -depth -max -scale 0.5
5136 
5137 '''
5138 
5139 try:
5140  import io_export_rogremesh.rogremesh as Rmesh
5141 except:
5142  Rmesh = None
5143  print( 'WARNING: "io_export_rogremesh" is missing' )
5144 
5145 if Rmesh and Rmesh.rpy.load():
5146  _USE_RPYTHON_ = True
5147 else:
5148  _USE_RPYTHON_ = False
5149  print( 'Rpython module is not cached, you must exit Blender to compile the module:' )
5150  print( 'cd io_export_rogremesh; python rogremesh.py' )
5151 
5152 class VertexNoPos(object):
5153  def __init__(self, ogre_vidx, nx,ny,nz, r,g,b,ra, vert_uvs):
5154  self.ogre_vidx = ogre_vidx
5155  self.nx = nx
5156  self.ny = ny
5157  self.nz = nz
5158  self.r = r
5159  self.g = g
5160  self.b = b
5161  self.ra = ra
5162  self.vert_uvs = vert_uvs
5163 
5164  '''does not compare ogre_vidx (and position at the moment) [ no need to compare position ]'''
5165  def __eq__(self, o):
5166  if self.nx != o.nx or self.ny != o.ny or self.nz != o.nz: return False
5167  elif self.r != o.r or self.g != o.g or self.b != o.b or self.ra != o.ra: return False
5168  elif len(self.vert_uvs) != len(o.vert_uvs): return False
5169  elif self.vert_uvs:
5170  for i, uv1 in enumerate( self.vert_uvs ):
5171  uv2 = o.vert_uvs[ i ]
5172  if uv1 != uv2: return False
5173  return True
5174 
5175 ## Creating .mesh
5176 
5177 def dot_mesh( ob, path='/tmp', force_name=None, ignore_shape_animation=False, normals=True, isLOD=False):
5178  start = time.time()
5179 
5180  logging = not isLOD
5181 
5182  if not os.path.isdir( path ):
5183  print('>> Creating working directory', path )
5184  os.makedirs( path )
5185 
5186  Report.meshes.append( ob.data.name )
5187  Report.faces += len( ob.data.tessfaces )
5188  Report.orig_vertices += len( ob.data.vertices )
5189 
5190  cleanup = False
5191  if ob.modifiers:
5192  cleanup = True
5193  copy = ob.copy()
5194  #bpy.context.scene.objects.link(copy)
5195  rem = []
5196  for mod in copy.modifiers: # remove armature and array modifiers before collaspe
5197  if mod.type in 'ARMATURE ARRAY'.split(): rem.append( mod )
5198  for mod in rem: copy.modifiers.remove( mod )
5199  # bake mesh
5200  mesh = copy.to_mesh(bpy.context.scene, True, "PREVIEW") # collaspe
5201  else:
5202  copy = ob
5203  mesh = ob.data
5204 
5205  name = force_name or ob.data.name
5206  name = clean_object_name(name)
5207  xmlfile = os.path.join(path, '%s.mesh.xml' % name )
5208 
5209  if logging:
5210  print(' - Generating:', '%s.mesh.xml' % name)
5211 
5212  if _USE_RPYTHON_ and False:
5213  Rmesh.save( ob, xmlfile )
5214  else:
5215  f = None
5216  try:
5217  f = open( xmlfile, 'w' )
5218  except Exception as e:
5219  show_dialog("Invalid mesh object name: " + name)
5220  return
5221 
5222  doc = SimpleSaxWriter(f, 'mesh', {})
5223 
5224  # Very ugly, have to replace number of vertices later
5225  doc.start_tag('sharedgeometry', {'vertexcount' : '__TO_BE_REPLACED_VERTEX_COUNT__'})
5226 
5227  if logging:
5228  print(' - Writing shared geometry')
5229 
5230  doc.start_tag('vertexbuffer', {
5231  'positions':'true',
5232  'normals':'true',
5233  'colours_diffuse' : str(bool( mesh.vertex_colors )),
5234  'texture_coords' : '%s' % len(mesh.uv_textures) if mesh.uv_textures.active else '0'
5235  })
5236 
5237  # Vertex colors
5238  vcolors = None
5239  vcolors_alpha = None
5240  if len( mesh.tessface_vertex_colors ):
5241  vcolors = mesh.tessface_vertex_colors[0]
5242  for bloc in mesh.tessface_vertex_colors:
5243  if bloc.name.lower().startswith('alpha'):
5244  vcolors_alpha = bloc; break
5245 
5246  # Materials
5247  materials = []
5248  for mat in ob.data.materials:
5249  if mat:
5250  materials.append( mat )
5251  else:
5252  print('[WARNING:] Bad material data in', ob)
5253  materials.append( '_missing_material_' ) # fixed dec22, keep proper index
5254  if not materials:
5255  materials.append( '_missing_material_' )
5256  _sm_faces_ = []
5257  for matidx, mat in enumerate( materials ):
5258  _sm_faces_.append([])
5259 
5260  # Textures
5261  dotextures = False
5262  uvcache = [] # should get a little speed boost by this cache
5263  if mesh.tessface_uv_textures.active:
5264  dotextures = True
5265  for layer in mesh.tessface_uv_textures:
5266  uvs = []; uvcache.append( uvs ) # layer contains: name, active, data
5267  for uvface in layer.data:
5268  uvs.append( (uvface.uv1, uvface.uv2, uvface.uv3, uvface.uv4) )
5269 
5270  _sm_vertices_ = {}
5271  _remap_verts_ = []
5272  numverts = 0
5273 
5274  for F in mesh.tessfaces:
5275  smooth = F.use_smooth
5276  faces = _sm_faces_[ F.material_index ]
5277  # Ogre only supports triangles
5278  tris = []
5279  tris.append( (F.vertices[0], F.vertices[1], F.vertices[2]) )
5280  if len(F.vertices) >= 4:
5281  tris.append( (F.vertices[0], F.vertices[2], F.vertices[3]) )
5282  if dotextures:
5283  a = []; b = []
5284  uvtris = [ a, b ]
5285  for layer in uvcache:
5286  uv1, uv2, uv3, uv4 = layer[ F.index ]
5287  a.append( (uv1, uv2, uv3) )
5288  b.append( (uv1, uv3, uv4) )
5289 
5290  for tidx, tri in enumerate(tris):
5291  face = []
5292  for vidx, idx in enumerate(tri):
5293  v = mesh.vertices[ idx ]
5294 
5295  if smooth:
5296  nx,ny,nz = swap( v.normal ) # fixed june 17th 2011
5297  else:
5298  nx,ny,nz = swap( F.normal )
5299 
5300  r = 1.0
5301  g = 1.0
5302  b = 1.0
5303  ra = 1.0
5304  if vcolors:
5305  k = list(F.vertices).index(idx)
5306  r,g,b = getattr( vcolors.data[ F.index ], 'color%s'%(k+1) )
5307  if vcolors_alpha:
5308  ra,ga,ba = getattr( vcolors_alpha.data[ F.index ], 'color%s'%(k+1) )
5309  else:
5310  ra = 1.0
5311 
5312  # Texture maps
5313  vert_uvs = []
5314  if dotextures:
5315  for layer in uvtris[ tidx ]:
5316  vert_uvs.append(layer[ vidx ])
5317 
5318  ''' Check if we already exported that vertex with same normal, do not export in that case,
5319  (flat shading in blender seems to work with face normals, so we copy each flat face'
5320  vertices, if this vertex with same normals was already exported,
5321  todo: maybe not best solution, check other ways (let blender do all the work, or only
5322  support smooth shading, what about seems, smoothing groups, materials, ...)
5323  '''
5324  vert = VertexNoPos(numverts, nx, ny, nz, r, g, b, ra, vert_uvs)
5325  alreadyExported = False
5326  if idx in _sm_vertices_:
5327  for vert2 in _sm_vertices_[idx]:
5328  #does not compare ogre_vidx (and position at the moment)
5329  if vert == vert2:
5330  face.append(vert2.ogre_vidx)
5331  alreadyExported = True
5332  #print(idx,numverts, nx,ny,nz, r,g,b,ra, vert_uvs, "already exported")
5333  break
5334  if not alreadyExported:
5335  face.append(vert.ogre_vidx)
5336  _sm_vertices_[idx].append(vert)
5337  #print(numverts, nx,ny,nz, r,g,b,ra, vert_uvs, "appended")
5338  else:
5339  face.append(vert.ogre_vidx)
5340  _sm_vertices_[idx] = [vert]
5341  #print(idx, numverts, nx,ny,nz, r,g,b,ra, vert_uvs, "created")
5342 
5343  if alreadyExported:
5344  continue
5345 
5346  numverts += 1
5347  _remap_verts_.append( v )
5348 
5349  x,y,z = swap(v.co) # xz-y is correct!
5350 
5351  doc.start_tag('vertex', {})
5352  doc.leaf_tag('position', {
5353  'x' : '%6f' % x,
5354  'y' : '%6f' % y,
5355  'z' : '%6f' % z
5356  })
5357 
5358  doc.leaf_tag('normal', {
5359  'x' : '%6f' % nx,
5360  'y' : '%6f' % ny,
5361  'z' : '%6f' % nz
5362  })
5363 
5364  if vcolors:
5365  doc.leaf_tag('colour_diffuse', {'value' : '%6f %6f %6f %6f' % (r,g,b,ra)})
5366 
5367  # Texture maps
5368  if dotextures:
5369  for uv in vert_uvs:
5370  doc.leaf_tag('texcoord', {
5371  'u' : '%6f' % uv[0],
5372  'v' : '%6f' % (1.0-uv[1])
5373  })
5374 
5375  doc.end_tag('vertex')
5376 
5377  faces.append( (face[0], face[1], face[2]) )
5378 
5379  Report.vertices += numverts
5380 
5381  doc.end_tag('vertexbuffer')
5382  doc.end_tag('sharedgeometry')
5383 
5384  if logging:
5385  print(' Done at', timer_diff_str(start), "seconds")
5386  print(' - Writing submeshes')
5387 
5388  doc.start_tag('submeshes', {})
5389  for matidx, mat in enumerate( materials ):
5390  if not len(_sm_faces_[matidx]):
5391  if not isinstance(mat, str):
5392  mat_name = mat.name
5393  else:
5394  mat_name = mat
5395  Report.warnings.append( 'BAD SUBMESH "%s": material %r, has not been applied to any faces - not exporting as submesh.' % (ob.name, mat_name) )
5396  continue # fixes corrupt unused materials
5397 
5398  submesh_attributes = {
5399  'usesharedvertices' : 'true',
5400  # Maybe better look at index of all faces, if one over 65535 set to true;
5401  # Problem: we know it too late, postprocessing of file needed
5402  "use32bitindexes" : str(bool(numverts > 65535)),
5403  "operationtype" : "triangle_list"
5404  }
5405  if material_name(mat, False) != "_missing_material_":
5406  submesh_attributes['material'] = material_name(mat, False)
5407 
5408  doc.start_tag('submesh', submesh_attributes)
5409  doc.start_tag('faces', {
5410  'count' : str(len(_sm_faces_[matidx]))
5411  })
5412  for fidx, (v1, v2, v3) in enumerate(_sm_faces_[matidx]):
5413  doc.leaf_tag('face', {
5414  'v1' : str(v1),
5415  'v2' : str(v2),
5416  'v3' : str(v3)
5417  })
5418  doc.end_tag('faces')
5419  doc.end_tag('submesh')
5420  Report.triangles += len(_sm_faces_[matidx])
5421 
5422  del(_sm_faces_)
5423  del(_sm_vertices_)
5424  doc.end_tag('submeshes')
5425 
5426  # Submesh names
5427  # todo: why is the submesh name taken from the material
5428  # when we have the blender object name available?
5429  doc.start_tag('submeshnames', {})
5430  for matidx, mat in enumerate( materials ):
5431  doc.leaf_tag('submesh', {
5432  'name' : material_name(mat, False),
5433  'index' : str(matidx)
5434  })
5435  doc.end_tag('submeshnames')
5436 
5437  if logging:
5438  print(' Done at', timer_diff_str(start), "seconds")
5439 
5440  # Generate lod levels
5441  if isLOD == False and ob.type == 'MESH' and CONFIG['lodLevels'] > 0:
5442  lod_levels = CONFIG['lodLevels']
5443  lod_distance = CONFIG['lodDistance']
5444  lod_ratio = CONFIG['lodPercent'] / 100.0
5445  lod_pre_mesh_count = len(bpy.data.meshes)
5446 
5447  # Cap lod levels to something sensible (what is it?)
5448  if lod_levels > 10:
5449  lod_levels = 10
5450 
5451  def activate_object(obj):
5452  bpy.ops.object.select_all(action = 'DESELECT')
5453  bpy.context.scene.objects.active = obj
5454  obj.select = True
5455 
5456  def duplicate_object(scene, name, copyobj):
5457 
5458  # Create new mesh
5459  mesh = bpy.data.meshes.new(name)
5460 
5461  # Create new object associated with the mesh
5462  ob_new = bpy.data.objects.new(name, mesh)
5463 
5464  # Copy data block from the old object into the new object
5465  ob_new.data = copyobj.data.copy()
5466  ob_new.location = copyobj.location
5467  ob_new.rotation_euler = copyobj.rotation_euler
5468  ob_new.scale = copyobj.scale
5469 
5470  # Link new object to the given scene and select it
5471  scene.objects.link(ob_new)
5472  ob_new.select = True
5473 
5474  return ob_new, mesh
5475 
5476  def delete_object(obj):
5477  activate_object(obj)
5478  bpy.ops.object.delete()
5479 
5480  # todo: Potential infinite recursion creation fails?
5481  def get_or_create_modifier(obj, modifier_name):
5482  if obj.type != 'MESH':
5483  return None
5484  # Find modifier
5485  for mod_iter in obj.modifiers:
5486  if mod_iter.type == modifier_name:
5487  return mod_iter
5488  # Not found? Create it and call recurse
5489  activate_object(obj)
5490  bpy.ops.object.modifier_add(type=modifier_name)
5491  return get_or_create_modifier(obj, modifier_name)
5492 
5493  # Create a temporary duplicate
5494  ob_copy, ob_copy_mesh = duplicate_object(bpy.context.scene, ob.name + "_LOD_TEMP_COPY", ob)
5495  ob_copy_meshes = [ ob_copy.data, ob_copy_mesh ]
5496 
5497  # Activate clone for modifier manipulation
5498  decimate = get_or_create_modifier(ob_copy, 'DECIMATE')
5499  if decimate is not None:
5500  decimate.decimate_type = 'COLLAPSE'
5501  decimate.show_viewport = True
5502  decimate.show_render = True
5503 
5504  lod_generated = []
5505  lod_ratio_multiplier = 1.0 - lod_ratio
5506  lod_current_ratio = 1.0 * lod_ratio_multiplier
5507  lod_current_distance = lod_distance
5508  lod_current_vertice_count = len(mesh.vertices)
5509  lod_min_vertice_count = 12
5510 
5511  for level in range(lod_levels+1)[1:]:
5512  decimate.ratio = lod_current_ratio
5513  lod_mesh = ob_copy.to_mesh(scene = bpy.context.scene, apply_modifiers = True, settings = 'PREVIEW')
5514  ob_copy_meshes.append(lod_mesh)
5515 
5516  # Check min vertice count and that the vertice count got reduced from last iteration
5517  lod_mesh_vertices = len(lod_mesh.vertices)
5518  if lod_mesh_vertices < lod_min_vertice_count:
5519  print(' - LOD', level, 'vertice count', lod_mesh_vertices, 'too small. Ignoring LOD.')
5520  break
5521  if lod_mesh_vertices >= lod_current_vertice_count:
5522  print(' - LOD', level-1, 'vertice count', lod_mesh_vertices, 'cannot be decimated any longer. Ignoring LOD.')
5523  break
5524  # todo: should we check if the ratio gets too small? although its up to the user to configure from the export panel
5525 
5526  lod_generated.append({ 'level': level, 'distance': lod_current_distance, 'ratio': lod_current_ratio, 'mesh': lod_mesh })
5527  lod_current_distance += lod_distance
5528  lod_current_vertice_count = lod_mesh_vertices
5529  lod_current_ratio *= lod_ratio_multiplier
5530 
5531  # Create lod .mesh files and generate LOD XML to the original .mesh.xml
5532  if len(lod_generated) > 0:
5533  # 'manual' means if the geometry gets loaded from a
5534  # different file that this LOD list references
5535  # NOTE: This is the approach at the moment. Another option would be to
5536  # references to the same vertex indexes in the shared geometry. But the
5537  # decimate approach wont work with this as it generates a fresh geometry.
5538  doc.start_tag('levelofdetail', {
5539  'strategy' : 'default',
5540  'numlevels' : str(len(lod_generated) + 1), # The main mesh is + 1 (kind of weird Ogre logic)
5541  'manual' : "true"
5542  })
5543 
5544  print(' - Generating', len(lod_generated), 'LOD meshes. Original: vertices', len(mesh.vertices), "faces", len(mesh.tessfaces))
5545  for lod in lod_generated:
5546  ratio_percent = round(lod['ratio'] * 100.0, 0)
5547  print(' > Writing LOD', lod['level'], 'for distance', lod['distance'], 'and ratio', str(ratio_percent) + "%", 'with', len(lod['mesh'].vertices), 'vertices', len(lod['mesh'].tessfaces), 'faces')
5548  lod_ob_temp = bpy.data.objects.new(name, lod['mesh'])
5549  lod_ob_temp.data.name = name + '_LOD_' + str(lod['level'])
5550  dot_mesh(lod_ob_temp, path, lod_ob_temp.data.name, ignore_shape_animation, normals, isLOD=True)
5551 
5552  # 'value' is the distance this LOD kicks in for the 'Distance' strategy.
5553  doc.leaf_tag('lodmanual', {
5554  'value' : str(lod['distance']),
5555  'meshname' : lod_ob_temp.data.name + ".mesh"
5556  })
5557 
5558  # Delete temporary LOD object.
5559  # The clone meshes will be deleted later.
5560  lod_ob_temp.user_clear()
5561  delete_object(lod_ob_temp)
5562  del lod_ob_temp
5563 
5564  doc.end_tag('levelofdetail')
5565 
5566  # Delete temporary LOD object
5567  delete_object(ob_copy)
5568  del ob_copy
5569 
5570  # Delete temporary data/mesh objects
5571  for mesh_iter in ob_copy_meshes:
5572  mesh_iter.user_clear()
5573  bpy.data.meshes.remove(mesh_iter)
5574  del mesh_iter
5575  ob_copy_meshes = []
5576 
5577  if lod_pre_mesh_count != len(bpy.data.meshes):
5578  print(' - WARNING: After LOD generation, cleanup failed to erase all temporary data!')
5579 
5580  arm = ob.find_armature()
5581  if arm:
5582  doc.leaf_tag('skeletonlink', {
5583  'name' : '%s.skeleton' % name
5584  })
5585  doc.start_tag('boneassignments', {})
5586  boneOutputEnableFromName = {}
5587  boneIndexFromName = {}
5588  for bone in arm.pose.bones:
5589  boneOutputEnableFromName[ bone.name ] = True
5590  if CONFIG['ONLY_DEFORMABLE_BONES']:
5591  # if we found a deformable bone,
5592  if bone.bone.use_deform:
5593  # visit all ancestor bones and mark them "output enabled"
5594  parBone = bone.parent
5595  while parBone:
5596  boneOutputEnableFromName[ parBone.name ] = True
5597  parBone = parBone.parent
5598  else:
5599  # non-deformable bone, no output
5600  boneOutputEnableFromName[ bone.name ] = False
5601  boneIndex = 0
5602  for bone in arm.pose.bones:
5603  boneIndexFromName[ bone.name ] = boneIndex
5604  if boneOutputEnableFromName[ bone.name ]:
5605  boneIndex += 1
5606  badverts = 0
5607  for vidx, v in enumerate(_remap_verts_):
5608  check = 0
5609  for vgroup in v.groups:
5610  if vgroup.weight > CONFIG['TRIM_BONE_WEIGHTS']:
5611  groupIndex = vgroup.group
5612  if groupIndex < len(copy.vertex_groups):
5613  vg = copy.vertex_groups[ groupIndex ]
5614  if vg.name in boneIndexFromName: # allows other vertex groups, not just armature vertex groups
5615  bnidx = boneIndexFromName[ vg.name ] # find_bone_index(copy,arm,vgroup.group)
5616  doc.leaf_tag('vertexboneassignment', {
5617  'vertexindex' : str(vidx),
5618  'boneindex' : str(bnidx),
5619  'weight' : '%6f' % vgroup.weight
5620  })
5621  check += 1
5622  else:
5623  print('WARNING: object vertex groups not in sync with armature', copy, arm, groupIndex)
5624  if check > 4:
5625  badverts += 1
5626  print('WARNING: vertex %s is in more than 4 vertex groups (bone weights)\n(this maybe Ogre incompatible)' %vidx)
5627  if badverts:
5628  Report.warnings.append( '%s has %s vertices weighted to too many bones (Ogre limits a vertex to 4 bones)\n[try increaseing the Trim-Weights threshold option]' %(mesh.name, badverts) )
5629  doc.end_tag('boneassignments')
5630 
5631  # Updated June3 2011 - shape animation works
5632  if CONFIG['SHAPE_ANIM'] and ob.data.shape_keys and len(ob.data.shape_keys.key_blocks):
5633  print(' - Writing shape keys')
5634 
5635  doc.start_tag('poses', {})
5636  for sidx, skey in enumerate(ob.data.shape_keys.key_blocks):
5637  if sidx == 0: continue
5638  if len(skey.data) != len( mesh.vertices ):
5639  failure = 'FAILED to save shape animation - you can not use a modifier that changes the vertex count! '
5640  failure += '[ mesh : %s ]' %mesh.name
5641  Report.warnings.append( failure )
5642  print( failure )
5643  break
5644 
5645  doc.start_tag('pose', {
5646  'name' : skey.name,
5647  # If target is 'mesh', no index needed, if target is submesh then submesh identified by 'index'
5648  #'index' : str(sidx-1),
5649  #'index' : '0',
5650  'target' : 'mesh'
5651  })
5652 
5653  for vidx, v in enumerate(_remap_verts_):
5654  pv = skey.data[ v.index ]
5655  x,y,z = swap( pv.co - v.co )
5656  #for i,p in enumerate( skey.data ):
5657  #x,y,z = p.co - ob.data.vertices[i].co
5658  #x,y,z = swap( ob.data.vertices[i].co - p.co )
5659  #if x==.0 and y==.0 and z==.0: continue # the older exporter optimized this way, is it safe?
5660  doc.leaf_tag('poseoffset', {
5661  'x' : '%6f' % x,
5662  'y' : '%6f' % y,
5663  'z' : '%6f' % z,
5664  'index' : str(vidx) # is this required?
5665  })
5666  doc.end_tag('pose')
5667  doc.end_tag('poses')
5668 
5669 
5670  if logging:
5671  print(' Done at', timer_diff_str(start), "seconds")
5672 
5673  if ob.data.shape_keys.animation_data and len(ob.data.shape_keys.animation_data.nla_tracks):
5674  print(' - Writing shape animations')
5675  doc.start_tag('animations', {})
5676  _fps = float( bpy.context.scene.render.fps )
5677  for nla in ob.data.shape_keys.animation_data.nla_tracks:
5678  for idx, strip in enumerate(nla.strips):
5679  doc.start_tag('animation', {
5680  'name' : strip.name,
5681  'length' : str((strip.frame_end-strip.frame_start)/_fps)
5682  })
5683  doc.start_tag('tracks', {})
5684  doc.start_tag('track', {
5685  'type' : 'pose',
5686  'target' : 'mesh'
5687  # If target is 'mesh', no index needed, if target is submesh then submesh identified by 'index'
5688  #'index' : str(idx)
5689  #'index' : '0'
5690  })
5691  doc.start_tag('keyframes', {})
5692  for frame in range( int(strip.frame_start), int(strip.frame_end)+1, bpy.context.scene.frame_step):#thanks to Vesa
5693  bpy.context.scene.frame_set(frame)
5694  doc.start_tag('keyframe', {
5695  'time' : str((frame-strip.frame_start)/_fps)
5696  })
5697  for sidx, skey in enumerate( ob.data.shape_keys.key_blocks ):
5698  if sidx == 0: continue
5699  doc.leaf_tag('poseref', {
5700  'poseindex' : str(sidx-1),
5701  'influence' : str(skey.value)
5702  })
5703  doc.end_tag('keyframe')
5704  doc.end_tag('keyframes')
5705  doc.end_tag('track')
5706  doc.end_tag('tracks')
5707  doc.end_tag('animation')
5708  doc.end_tag('animations')
5709  print(' Done at', timer_diff_str(start), "seconds")
5710 
5711  ## Clean up and save
5712  #bpy.context.scene.meshes.unlink(mesh)
5713  if cleanup:
5714  #bpy.context.scene.objects.unlink(copy)
5715  copy.user_clear()
5716  bpy.data.objects.remove(copy)
5717  mesh.user_clear()
5718  bpy.data.meshes.remove(mesh)
5719  del copy
5720  del mesh
5721  del _remap_verts_
5722  del uvcache
5723  doc.close() # reported by Reyn
5724  f.close()
5725 
5726  if logging:
5727  print(' - Created .mesh.xml at', timer_diff_str(start), "seconds")
5728 
5729  # todo: Very ugly, find better way
5730  def replaceInplace(f,searchExp,replaceExp):
5731  import fileinput
5732  for line in fileinput.input(f, inplace=1):
5733  if searchExp in line:
5734  line = line.replace(searchExp,replaceExp)
5735  sys.stdout.write(line)
5736  fileinput.close() # reported by jakob
5737 
5738  replaceInplace(xmlfile, '__TO_BE_REPLACED_VERTEX_COUNT__' + '"', str(numverts) + '"' )#+ ' ' * (ls - lr))
5739  del(replaceInplace)
5740 
5741  # Start .mesh.xml to .mesh convertion tool
5742  OgreXMLConverter(xmlfile, has_uvs=dotextures)
5743 
5744  if arm and CONFIG['ARM_ANIM']:
5745  skel = Skeleton( ob )
5746  data = skel.to_xml()
5747  name = force_name or ob.data.name
5748  name = clean_object_name(name)
5749  xmlfile = os.path.join(path, '%s.skeleton.xml' % name)
5750  f = open( xmlfile, 'wb' )
5751  f.write( bytes(data,'utf-8') )
5752  f.close()
5753  OgreXMLConverter( xmlfile )
5754 
5755  mats = []
5756  for mat in materials:
5757  if mat != '_missing_material_':
5758  mats.append(mat)
5759 
5760  if logging:
5761  print(' - Created .mesh in total time', timer_diff_str(start), 'seconds')
5762  return mats
5763 
5764 ## Jmonkey preview
5765 ## todo: remove jmonkey
5766 
5767 class JmonkeyPreviewOp( _OgreCommonExport_, bpy.types.Operator ):
5768  '''helper to open jMonkey (JME)'''
5769  bl_idname = 'jmonkey.preview'
5770  bl_label = "opens JMonkeyEngine in a non-blocking subprocess"
5771  bl_options = {'REGISTER'}
5772 
5773  filepath= StringProperty(name="File Path", description="Filepath used for exporting Jmonkey .scene file", maxlen=1024, default="/tmp/preview.txml", subtype='FILE_PATH')
5774  EXPORT_TYPE = 'OGRE'
5775 
5776  @classmethod
5777  def poll(cls, context):
5778  if context.active_object: return True
5779 
5780  def invoke(self, context, event):
5781  global TundraSingleton
5782  path = '/tmp/preview.scene'
5783  self.ogre_export( path, context )
5784  JmonkeyPipe( path )
5785  return {'FINISHED'}
5786 
5787 def JmonkeyPipe( path ):
5788  root = CONFIG[ 'JMONKEY_ROOT']
5789  if sys.platform.startswith('win'):
5790  cmd = [ os.path.join( os.path.join( root, 'bin' ), 'jmonkeyplatform.exe' ) ]
5791  else:
5792  cmd = [ os.path.join( os.path.join( root, 'bin' ), 'jmonkeyplatform' ) ]
5793  cmd.append( '--nosplash' )
5794  cmd.append( '--open' )
5795  cmd.append( path )
5796  proc = subprocess.Popen(cmd)#, stdin=subprocess.PIPE)
5797  return proc
5798 
5799 ## realXtend Tundra preview
5800 ## todo: This only work if the custom py script is enabled in Tundra
5801 ## It's nice when it works but PythonScriptModule is not part of the
5802 ## default Tundra distro anymore, so this is atm kind of dead.
5803 
5804 class TundraPreviewOp( _OgreCommonExport_, bpy.types.Operator ):
5805  '''helper to open Tundra2 (realXtend)'''
5806  bl_idname = 'tundra.preview'
5807  bl_label = "opens Tundra2 in a non-blocking subprocess"
5808  bl_options = {'REGISTER'}
5809  EXPORT_TYPE = 'REX'
5810 
5811  filepath= StringProperty(
5812  name="File Path",
5813  description="Filepath used for exporting Tundra .txml file",
5814  maxlen=1024,
5815  default="/tmp/preview.txml",
5816  subtype='FILE_PATH')
5817  EX_FORCE_CAMERA = BoolProperty(
5818  name="Force Camera",
5819  description="export active camera",
5820  default=False)
5821  EX_FORCE_LAMPS = BoolProperty(
5822  name="Force Lamps",
5823  description="export all lamps",
5824  default=False)
5825 
5826  @classmethod
5827  def poll(cls, context):
5828  if context.active_object and context.mode != 'EDIT_MESH':
5829  return True
5830 
5831  def invoke(self, context, event):
5832  global TundraSingleton
5833  syncmats = []
5834  obs = []
5835  if TundraSingleton:
5836  actob = context.active_object
5837  obs = TundraSingleton.deselect_previously_updated(context)
5838  for ob in obs:
5839  if ob.type=='MESH':
5840  syncmats.append( ob )
5841  if ob.name == actob.name:
5842  export_mesh( ob, path='/tmp/rex' )
5843 
5844  if not os.path.isdir( '/tmp/rex' ): os.makedirs( '/tmp/rex' )
5845  path = '/tmp/rex/preview.txml'
5846  self.ogre_export( path, context, force_material_update=syncmats )
5847  if not TundraSingleton:
5848  TundraSingleton = TundraPipe( context )
5849  elif self.EX_SCENE:
5850  TundraSingleton.load( context, path )
5851 
5852  for ob in obs:
5853  ob.select = True # restore selection
5854  return {'FINISHED'}
5855 
5856 TundraSingleton = None
5857 
5858 class Tundra_StartPhysicsOp(bpy.types.Operator):
5859  '''TundraSingleton helper'''
5860  bl_idname = 'tundra.start_physics'
5861  bl_label = "start physics"
5862  bl_options = {'REGISTER'}
5863 
5864  @classmethod
5865  def poll(cls, context):
5866  if TundraSingleton: return True
5867  def invoke(self, context, event):
5868  TundraSingleton.start()
5869  return {'FINISHED'}
5870 
5871 class Tundra_StopPhysicsOp(bpy.types.Operator):
5872  '''TundraSingleton helper'''
5873  bl_idname = 'tundra.stop_physics'
5874  bl_label = "stop physics"
5875  bl_options = {'REGISTER'}
5876 
5877  @classmethod
5878  def poll(cls, context):
5879  if TundraSingleton: return True
5880  def invoke(self, context, event):
5881  TundraSingleton.stop()
5882  return {'FINISHED'}
5883 
5884 class Tundra_PhysicsDebugOp(bpy.types.Operator):
5885  '''TundraSingleton helper'''
5886  bl_idname = 'tundra.toggle_physics_debug'
5887  bl_label = "stop physics"
5888  bl_options = {'REGISTER'}
5889 
5890  @classmethod
5891  def poll(cls, context):
5892  if TundraSingleton: return True
5893  def invoke(self, context, event):
5894  TundraSingleton.toggle_physics_debug()
5895  return {'FINISHED'}
5896 
5897 class Tundra_ExitOp(bpy.types.Operator):
5898  '''TundraSingleton helper'''
5899  bl_idname = 'tundra.exit'
5900  bl_label = "quit tundra"
5901  bl_options = {'REGISTER'}
5902 
5903  @classmethod
5904  def poll(cls, context):
5905  if TundraSingleton: return True
5906  def invoke(self, context, event):
5907  TundraSingleton.exit()
5908  return {'FINISHED'}
5909 
5910 ## Server object to talk with realXtend Tundra with UDP
5911 ## Requires Tundra to be running a py script.
5912 
5913 class Server(object):
5914  def stream( self, o ):
5915  b = pickle.dumps( o, protocol=2 ) #protocol2 is python2 compatible
5916  #print( 'streaming bytes', len(b) )
5917  n = len( b ); d = STREAM_BUFFER_SIZE - n -4
5918  if n > STREAM_BUFFER_SIZE:
5919  print( 'ERROR: STREAM OVERFLOW:', n )
5920  return
5921  padding = b'#' * d
5922  if n < 10: header = '000%s' %n
5923  elif n < 100: header = '00%s' %n
5924  elif n < 1000: header = '0%s' %n
5925  else: header = '%s' %n
5926  header = bytes( header, 'utf-8' )
5927  assert len(header) == 4
5928  w = header + b + padding
5929  assert len(w) == STREAM_BUFFER_SIZE
5930  self.buffer.insert(0, w )
5931  return w
5932 
5933  def multires_lod( self ):
5934  '''
5935  Ogre builtin LOD sucks for character animation
5936  '''
5937  ob = bpy.context.active_object
5938  cam = bpy.context.scene.camera
5939 
5940  if ob and cam and ob.type=='MESH' and ob.use_multires_lod:
5941  delta = bpy.context.active_object.matrix_world.to_translation() - cam.matrix_world.to_translation()
5942  dist = delta.length
5943  #print( 'Distance', dist )
5944  if ob.modifiers and ob.modifiers[0].type == 'MULTIRES' and ob.modifiers[0].total_levels > 1:
5945  mod = ob.modifiers[0]
5946  step = ob.multires_lod_range / mod.total_levels
5947  level = mod.total_levels - int( dist / step )
5948  if mod.levels != level: mod.levels = level
5949  return level
5950 
5951  def sync( self ): # 153 bytes per object + n bytes for animation names and weights
5952  LOD = self.multires_lod()
5953 
5954  p = STREAM_PROTO
5955  i = 0; msg = []
5956  for ob in bpy.context.selected_objects:
5957  if ob.type not in ('MESH','LAMP','SPEAKER'): continue
5958  loc, rot, scale = ob.matrix_world.decompose()
5959  loc = swap(loc).to_tuple()
5960  x,y,z = swap( rot.to_euler() )
5961  rot = (x,y,z)
5962  x,y,z = swap( scale )
5963  scale = ( abs(x), abs(y), abs(z) )
5964  d = { p['ID']:uid(ob), p['POSITION']:loc, p['ROTATION']:rot, p['SCALE']:scale, p['TYPE']:p[ob.type] }
5965  msg.append( d )
5966 
5967  if ob.name == bpy.context.active_object.name and LOD is not None:
5968  d[ p['LOD'] ] = LOD
5969 
5970  if ob.type == 'MESH':
5971  arm = ob.find_armature()
5972  if arm and arm.animation_data and arm.animation_data.nla_tracks:
5973  anim = None
5974  d[ p['ANIMATIONS'] ] = state = {} # animation-name : weight
5975  for nla in arm.animation_data.nla_tracks:
5976  for strip in nla.strips:
5977  if strip.active: state[ strip.name ] = strip.influence
5978  else: pass # armature without proper NLA setup
5979  elif ob.type == 'LAMP':
5980  d[ p['ENERGY'] ] = ob.data.energy
5981  d[ p['DISTANCE'] ] = ob.data.distance
5982  elif ob.type == 'SPEAKER':
5983  d[ p['VOLUME'] ] = ob.data.volume
5984  d[ p['MUTE'] ] = ob.data.muted
5985 
5986  if i >= 10: break # max is 13 objects to stay under 2048 bytes
5987  return msg
5988 
5989  def __init__(self):
5990  import socket
5991  self.buffer = [] # cmd buffer
5992  self.socket = sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) # UDP
5993  host='localhost'; port = 9420
5994  sock.connect((host, port))
5995  print('SERVER: socket connected', sock)
5996  self._handle = None
5997  self.setup_callback( bpy.context )
5998  import threading
5999  self.ready = threading._allocate_lock()
6000  self.ID = threading._start_new_thread(
6001  self.loop, (None,)
6002  )
6003  print( 'SERVER: thread started')
6004 
6005  def loop(self, none):
6006  self.active = True
6007  prev = time.time()
6008  while self.active:
6009  if not self.ready.locked(): time.sleep(0.001) # not threadsafe
6010  else: # threadsafe start
6011  now = time.time()
6012  if now - prev > 0.066: # don't flood Tundra
6013  actob = None
6014  try: actob = bpy.context.active_object
6015  except: pass
6016  if not actob: continue
6017  prev = now
6018  sel = bpy.context.active_object
6019  msg = self.sync()
6020  self.ready.release() # thread release
6021  self.stream( msg ) # releases GIL?
6022  if self.buffer:
6023  bin = self.buffer.pop()
6024  try:
6025  self.socket.sendall( bin )
6026  except:
6027  print('SERVER: send data error')
6028  time.sleep(0.5)
6029  pass
6030  else: print( 'SERVER: empty buffer' )
6031  else:
6032  self.ready.release()
6033  print('SERVER: thread exit')
6034 
6035  def threadsafe( self, reg ):
6036  if not TundraSingleton: return
6037  if not self.ready.locked():
6038  self.ready.acquire()
6039  time.sleep(0.0001)
6040  while self.ready.locked(): # must block to be safe
6041  time.sleep(0.0001) # wait for unlock
6042  else: pass #time.sleep(0.033) dont block
6043 
6044  _handle = None
6045  def setup_callback( self, context ): # TODO replace with a proper frame update callback
6046  print('SERVER: setup frame update callback')
6047  if self._handle: return self._handle
6048  for area in bpy.context.window.screen.areas:
6049  if area.type == 'VIEW_3D':
6050  for reg in area.regions:
6051  if reg.type == 'WINDOW':
6052  # PRE_VIEW, POST_VIEW, POST_PIXEL
6053  self._handle = reg.callback_add(self.threadsafe, (reg,), 'PRE_VIEW' )
6054  self._area = area
6055  self._region = reg
6056  break
6057  if not self._handle:
6058  print('SERVER: FAILED to setup frame update callback')
6059 
6060 def _create_stream_proto():
6061  proto = {}
6062  tags = 'ID NAME POSITION ROTATION SCALE DATA SELECTED TYPE MESH LAMP CAMERA SPEAKER ANIMATIONS DISTANCE ENERGY VOLUME MUTE LOD'.split()
6063  for i,tag in enumerate( tags ):
6064  proto[ tag ] = chr(i) # up to 256
6065  return proto
6066 
6067 STREAM_PROTO = _create_stream_proto()
6068 STREAM_BUFFER_SIZE = 2048
6069 
6070 TUNDRA_SCRIPT = '''
6071 # this file was generated by blender2ogre #
6072 import tundra, socket, select, pickle
6073 STREAM_BUFFER_SIZE = 2048
6074 globals().update( %s )
6075 E = {} # this is just for debugging from the pyconsole
6076 
6077 def get_entity(ID):
6078  scn = tundra.Scene().MainCameraScene()
6079  return scn.GetEntityRaw( ID )
6080 
6081 class Client(object):
6082  def __init__(self):
6083  self.socket = sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
6084  host='localhost'; port = 9420
6085  sock.bind((host, port))
6086  self._animated = {} # entity ID : { anim-name : weight }
6087 
6088  def update(self, delay):
6089  global E
6090  sock = self.socket
6091  poll = select.select( [ sock ], [], [], 0.01 )
6092  if not poll[0]: return True
6093  data = sock.recv( STREAM_BUFFER_SIZE )
6094  assert len(data) == STREAM_BUFFER_SIZE
6095  if not data:
6096  print( 'blender crashed?' )
6097  return
6098  header = data[ : 4]
6099  s = data[ 4 : int(header)+4 ]
6100  objects = pickle.loads( s )
6101  scn = tundra.Scene().MainCameraScene() # replaces GetDefaultScene()
6102  for ob in objects:
6103  e = scn.GetEntityRaw( ob[ID] )
6104  if not e: continue
6105  x,y,z = ob[POSITION]
6106  e.placeable.SetPosition( x,y,z )
6107  x,y,z = ob[SCALE]
6108  e.placeable.SetScale( x,y,z )
6109  #e.placeable.SetOrientation( ob[ROTATION] )
6110 
6111  if ob[TYPE] == LAMP:
6112  e.light.range = ob[ DISTANCE ]
6113  e.light.brightness = ob[ ENERGY ]
6114  #e.light.diffColor = !! not wrapped !!
6115  #e.light.specColor = !! not wrapped !!
6116  elif ob[TYPE] == SPEAKER:
6117  e.sound.soundGain = ob[VOLUME]
6118  #e.sound.soundInnerRadius =
6119  #e.sound.soundOuterRadius =
6120  if ob[MUTE]: e.sound.StopSound()
6121  else: e.sound.PlaySound() # tundra API needs sound.IsPlaying()
6122 
6123  if ANIMATIONS in ob:
6124  self.update_animation( e, ob )
6125 
6126  if LOD in ob:
6127  #print( 'LOD', ob[LOD] )
6128  index = e.id + ob[LOD] + 1
6129  for i in range(1,9):
6130  elod = get_entity( e.id + i )
6131  if elod:
6132  if elod.id == index and not elod.placeable.visible:
6133  elod.placeable.visible = True
6134  elif elod.id != index and elod.placeable.visible:
6135  elod.placeable.visible = False
6136 
6137  if ob[ID] not in E: E[ ob[ID] ] = e
6138 
6139  def update_animation( self, e, ob ):
6140  if ob[ID] not in self._animated:
6141  self._animated[ ob[ID] ] = {}
6142  state = self._animated[ ob[ID] ]
6143  ac = e.animationcontroller
6144  for aname in ob[ ANIMATIONS ]:
6145  if aname not in state: # save weight of new animation
6146  state[ aname ] = ob[ANIMATIONS][aname] # weight
6147  for aname in state:
6148  if aname not in ob[ANIMATIONS] and ac.IsAnimationActive( aname ):
6149  ac.StopAnim( aname, '0.0' )
6150  elif aname in ob[ANIMATIONS]:
6151  weight = ob[ANIMATIONS][aname]
6152  if ac.HasAnimationFinished( aname ):
6153  ac.PlayLoopedAnim( aname, '1.0', 'false' ) # PlayAnim(...) TODO single playback
6154  ok = ac.SetAnimationWeight( aname, weight )
6155  state[ aname ] = weight
6156 
6157  if weight != state[ aname ]:
6158  ok = ac.SetAnimationWeight( aname, weight )
6159  state[ aname ] = weight
6160 
6161 client = Client()
6162 tundra.Frame().connect( 'Updated(float)', client.update )
6163 print('blender2ogre plugin ok')
6164 ''' %STREAM_PROTO
6165 
6166 class TundraPipe(object):
6167  CONFIG_PATH = '/tmp/rex/plugins.xml'
6168  TUNDRA_SCRIPT_PATH = '/tmp/rex/b2ogre_plugin.py'
6169  CONFIG_XML = '''<?xml version="1.0"?>
6170 <Tundra>
6171  <!-- plugins.xml is hardcoded to be the default configuration file to load if another file is not specified on the command line with the "config filename.xml" parameter. -->
6172  <plugin path="OgreRenderingModule" />
6173  <plugin path="EnvironmentModule" /> <!-- EnvironmentModule depends on OgreRenderingModule -->
6174  <plugin path="PhysicsModule" /> <!-- PhysicsModule depends on OgreRenderingModule and EnvironmentModule -->
6175  <plugin path="TundraProtocolModule" /> <!-- TundraProtocolModule depends on OgreRenderingModule -->
6176  <plugin path="JavascriptModule" /> <!-- JavascriptModule depends on TundraProtocolModule -->
6177  <plugin path="AssetModule" /> <!-- AssetModule depends on TundraProtocolModule -->
6178  <plugin path="AvatarModule" /> <!-- AvatarModule depends on AssetModule and OgreRenderingModule -->
6179  <plugin path="ECEditorModule" /> <!-- ECEditorModule depends on OgreRenderingModule, TundraProtocolModule, OgreRenderingModule and AssetModule -->
6180  <plugin path="SkyXHydrax" /> <!-- SkyXHydrax depends on OgreRenderingModule -->
6181  <plugin path="OgreAssetEditorModule" /> <!-- OgreAssetEditorModule depends on OgreRenderingModule -->
6182  <plugin path="DebugStatsModule" /> <!-- DebugStatsModule depends on OgreRenderingModule, EnvironmentModule and AssetModule -->
6183  <plugin path="SceneWidgetComponents" /> <!-- SceneWidgetComponents depends on OgreRenderingModule and TundraProtocolModule -->
6184  <plugin path="PythonScriptModule" />
6185 
6186  <!-- TODO: Currently the above <plugin> items are loaded in the order they are specified, but below, the jsplugin items are loaded in an undefined order. Use the order specified here as the load order. -->
6187  <!-- NOTE: The startup .js scripts are specified only by base name of the file. Don's specify a path here. Place the startup .js scripts to /bin/jsmodules/startup/. -->
6188  <!-- Important: The file names specified here are case sensitive even on Windows! -->
6189  <jsplugin path="cameraapplication.js" />
6190  <jsplugin path="FirstPersonMouseLook.js" />
6191  <jsplugin path="MenuBar.js" />
6192 
6193  <!-- Python plugins -->
6194  <!-- <pyplugin path="lib/apitests.py" /> --> <!-- Runs framework api tests -->
6195  <pyplugin path="%s" /> <!-- shows qt py console. could enable by default when add to menu etc. for controls, now just shows directly when is enabled here -->
6196 
6197  <option name="--accept_unknown_http_sources" />
6198  <option name="--accept_unknown_local_sources" />
6199  <option name="--fpslimit" value="60" />
6200  <!-- AssetAPI's file system watcher currently disabled because LocalAssetProvider implements
6201  the same functionality for LocalAssetStorages and HTTPAssetProviders do not yet support live-update. -->
6202  <option name="--nofilewatcher" />
6203 
6204 </Tundra>''' %TUNDRA_SCRIPT_PATH
6205 
6206  def __init__(self, context, debug=False):
6207  self._physics_debug = True
6208  self._objects = []
6209  self.proc = None
6210  exe = None
6211  if 'Tundra.exe' in os.listdir( CONFIG['TUNDRA_ROOT'] ):
6212  exe = os.path.join( CONFIG['TUNDRA_ROOT'], 'Tundra.exe' )
6213  elif 'Tundra' in os.listdir( CONFIG['TUNDRA_ROOT'] ):
6214  exe = os.path.join( CONFIG['TUNDRA_ROOT'], 'Tundra' )
6215 
6216  cmd = []
6217  if not exe:
6218  print('ERROR: failed to find Tundra executable')
6219  assert 0
6220  elif sys.platform.startswith('win'):
6221  cmd.append(exe)
6222  else:
6223  if exe.endswith('.exe'): cmd.append('wine') # assume user has Wine
6224  cmd.append( exe )
6225  if debug:
6226  cmd.append('--loglevel')
6227  cmd.append('debug')
6228 
6229  if CONFIG['TUNDRA_STREAMING']:
6230  cmd.append( '--config' )
6231  cmd.append( self.CONFIG_PATH )
6232  with open( self.CONFIG_PATH, 'wb' ) as fp: fp.write( bytes(self.CONFIG_XML,'utf-8') )
6233  with open( self.TUNDRA_SCRIPT_PATH, 'wb' ) as fp: fp.write( bytes(TUNDRA_SCRIPT,'utf-8') )
6234  self.server = Server()
6235 
6236  #cmd += ['--file', '/tmp/rex/preview.txml'] # tundra2.1.2 bug loading from --file ignores entity ID's
6237  cmd.append( '--storage' )
6238  if sys.platform.startswith('win'): cmd.append( 'C:\\tmp\\rex' )
6239  else: cmd.append( '/tmp/rex' )
6240  self.proc = subprocess.Popen(cmd, stdin=subprocess.PIPE, cwd=CONFIG['TUNDRA_ROOT'])
6241 
6242  self.physics = True
6243  if self.proc:
6244  time.sleep(0.1)
6245  self.load( context, '/tmp/rex/preview.txml' )
6246  self.stop()
6247 
6248  def deselect_previously_updated(self, context):
6249  r = []
6250  for ob in context.selected_objects:
6251  if ob.name in self._objects: ob.select = False; r.append( ob )
6252  return r
6253 
6254  def load( self, context, url, clear=False ):
6255  self._objects += [ob.name for ob in context.selected_objects]
6256  if clear:
6257  self.proc.stdin.write( b'loadscene(/tmp/rex/preview.txml,true,true)\n')
6258  else:
6259  self.proc.stdin.write( b'loadscene(/tmp/rex/preview.txml,false,true)\n')
6260  try:
6261  self.proc.stdin.flush()
6262  except:
6263  global TundraSingleton
6264  TundraSingleton = None
6265 
6266  def start( self ):
6267  self.physics = True
6268  self.proc.stdin.write( b'startphysics\n' )
6269  try: self.proc.stdin.flush()
6270  except:
6271  global TundraSingleton
6272  TundraSingleton = None
6273 
6274  def stop( self ):
6275  self.physics = False
6276  self.proc.stdin.write( b'stopphysics\n' )
6277  try: self.proc.stdin.flush()
6278  except:
6279  global TundraSingleton
6280  TundraSingleton = None
6281 
6282  def toggle_physics_debug( self ):
6283  self._physics_debug = not self._physics_debug
6284  self.proc.stdin.write( b'physicsdebug\n' )
6285  try: self.proc.stdin.flush()
6286  except:
6287  global TundraSingleton
6288  TundraSingleton = None
6289 
6290  def exit(self):
6291  self.proc.stdin.write( b'exit\n' )
6292  self.proc.stdin.flush()
6293  global TundraSingleton
6294  TundraSingleton = None
6295 
6296 
6297 ## More UI
6298 
6299 class MENU_preview_material_text(bpy.types.Menu):
6300  bl_label = 'preview'
6301 
6302  @classmethod
6303  def poll(self,context):
6304  if context.active_object and context.active_object.active_material:
6305  return True
6306 
6307  def draw(self, context):
6308  layout = self.layout
6309  mat = context.active_object.active_material
6310  if mat:
6311  #CONFIG['TOUCH_TEXTURES'] = False
6312  preview = generate_material( mat )
6313  for line in preview.splitlines():
6314  if line.strip():
6315  for ww in wordwrap( line ):
6316  layout.label(text=ww)
6317 
6318 @UI
6319 class INFO_HT_myheader(bpy.types.Header):
6320  bl_space_type = 'INFO'
6321  def draw(self, context):
6322  layout = self.layout
6323  wm = context.window_manager
6324  window = context.window
6325  scene = context.scene
6326  rd = scene.render
6327  ob = context.active_object
6328  screen = context.screen
6329 
6330  #layout.separator()
6331 
6332  if _USE_JMONKEY_:
6333  row = layout.row(align=True)
6334  op = row.operator( 'jmonkey.preview', text='', icon='MONKEY' )
6335 
6336  if _USE_TUNDRA_:
6337  row = layout.row(align=True)
6338  op = row.operator( 'tundra.preview', text='', icon='WORLD' )
6339  if TundraSingleton:
6340  op = row.operator( 'tundra.preview', text='', icon='META_CUBE' )
6341  op.EX_SCENE = False
6342  if not TundraSingleton.physics:
6343  op = row.operator( 'tundra.start_physics', text='', icon='PLAY' )
6344  else:
6345  op = row.operator( 'tundra.stop_physics', text='', icon='PAUSE' )
6346  op = row.operator( 'tundra.toggle_physics_debug', text='', icon='MOD_PHYSICS' )
6347  op = row.operator( 'tundra.exit', text='', icon='CANCEL' )
6348 
6349  op = layout.operator( 'ogremeshy.preview', text='', icon='PLUGIN' ); op.mesh = True
6350 
6351  row = layout.row(align=True)
6352  sub = row.row(align=True)
6353  sub.menu("INFO_MT_file")
6354  sub.menu("INFO_MT_add")
6355  if rd.use_game_engine: sub.menu("INFO_MT_game")
6356  else: sub.menu("INFO_MT_render")
6357 
6358  row = layout.row(align=False); row.scale_x = 1.25
6359  row.menu("INFO_MT_instances", icon='NODETREE', text='')
6360  row.menu("INFO_MT_groups", icon='GROUP', text='')
6361 
6362  layout.template_header()
6363  if not context.area.show_menus:
6364  if window.screen.show_fullscreen: layout.operator("screen.back_to_previous", icon='SCREEN_BACK', text="Back to Previous")
6365  else: layout.template_ID(context.window, "screen", new="screen.new", unlink="screen.delete")
6366  layout.template_ID(context.screen, "scene", new="scene.new", unlink="scene.delete")
6367 
6368  layout.separator()
6369  layout.template_running_jobs()
6370  layout.template_reports_banner()
6371  layout.separator()
6372  if rd.has_multiple_engines: layout.prop(rd, "engine", text="")
6373 
6374  layout.label(text=scene.statistics())
6375  layout.menu( "INFO_MT_help" )
6376  else:
6377  layout.template_ID(context.window, "screen", new="screen.new", unlink="screen.delete")
6378 
6379  if ob:
6380  row = layout.row(align=True)
6381  row.prop( ob, 'name', text='' )
6382  row.prop( ob, 'draw_type', text='' )
6383  row.prop( ob, 'show_x_ray', text='' )
6384  row = layout.row()
6385  row.scale_y = 0.75; row.scale_x = 0.9
6386  row.prop( ob, 'layers', text='' )
6387 
6388  layout.separator()
6389  row = layout.row(align=True); row.scale_x = 1.1
6390  row.prop(scene.game_settings, 'material_mode', text='')
6391  row.prop(scene, 'camera', text='')
6392 
6393  layout.menu( 'MENU_preview_material_text', icon='TEXT', text='' )
6394 
6395  layout.menu( "INFO_MT_ogre_docs" )
6396  layout.operator("wm.window_fullscreen_toggle", icon='FULLSCREEN_ENTER', text="")
6397  if OgreToggleInterfaceOp.TOGGLE: layout.operator('ogre.toggle_interface', text='Ogre', icon='CHECKBOX_DEHLT')
6398  else: layout.operator('ogre.toggle_interface', text='Ogre', icon='CHECKBOX_HLT')
6399 
6400 def export_menu_func_ogre(self, context):
6401  op = self.layout.operator(INFO_OT_createOgreExport.bl_idname, text="Ogre3D (.scene and .mesh)")
6402 
6403 def export_menu_func_realxtend(self, context):
6404  op = self.layout.operator(INFO_OT_createRealxtendExport.bl_idname, text="realXtend Tundra (.txml and .mesh)")
6405 
6406 try:
6407  _header_ = bpy.types.INFO_HT_header
6408 except:
6409  print('---blender2ogre addon enable---')
6410 
6411 ## Toggle button for blender2ogre UI panels
6412 
6413 class OgreToggleInterfaceOp(bpy.types.Operator):
6414  '''Toggle Ogre UI'''
6415  bl_idname = "ogre.toggle_interface"
6416  bl_label = "Ogre UI"
6417  bl_options = {'REGISTER'}
6418  TOGGLE = True #restore_minimal_interface()
6419  BLENDER_DEFAULT_HEADER = _header_
6420 
6421  @classmethod
6422  def poll(cls, context):
6423  return True
6424 
6425  def invoke(self, context, event):
6426  #global _header_
6427  if OgreToggleInterfaceOp.TOGGLE: #_header_:
6428  print( 'ogre.toggle_interface ENABLE' )
6429  bpy.utils.register_module(__name__)
6430  #_header_ = bpy.types.INFO_HT_header
6431  try: bpy.utils.unregister_class(_header_)
6432  except: pass
6433  bpy.utils.unregister_class( INFO_HT_microheader ) # moved to custom header
6434  OgreToggleInterfaceOp.TOGGLE = False
6435  else:
6436  print( 'ogre.toggle_interface DISABLE' )
6437  #bpy.utils.unregister_module(__name__); # this is not safe, can segfault blender, why?
6439  bpy.utils.register_class(_header_)
6440  restore_minimal_interface()
6441  OgreToggleInterfaceOp.TOGGLE = True
6442  return {'FINISHED'}
6443 
6444 class INFO_HT_microheader(bpy.types.Header):
6445  bl_space_type = 'INFO'
6446  def draw(self, context):
6447  layout = self.layout
6448  try:
6449  if OgreToggleInterfaceOp.TOGGLE:
6450  layout.operator('ogre.toggle_interface', text='Ogre', icon='CHECKBOX_DEHLT')
6451  else:
6452  layout.operator('ogre.toggle_interface', text='Ogre', icon='CHECKBOX_HLT')
6453  except: pass # STILL REQUIRED?
6454 
6455 def get_minimal_interface_classes():
6456  return INFO_OT_createOgreExport, INFO_OT_createRealxtendExport, OgreToggleInterfaceOp, MiniReport, INFO_HT_microheader
6457 
6458 _USE_TUNDRA_ = False
6459 _USE_JMONKEY_ = False
6460 
6461 def restore_minimal_interface():
6462  #if not hasattr( bpy.ops.ogre.. #always true
6463  for cls in get_minimal_interface_classes():
6464  try: bpy.utils.register_class( cls )
6465  except: pass
6466  return False
6467 
6468  try:
6469  bpy.utils.register_class( INFO_HT_microheader )
6470  for op in get_minimal_interface_classes(): bpy.utils.register_class( op )
6471  return False
6472  except:
6473  print( 'b2ogre minimal UI already setup' )
6474  return True
6475 
6476 MyShaders = None
6477 
6478 def register():
6479  print('Starting blender2ogre', VERSION)
6480  global MyShaders, _header_, _USE_TUNDRA_, _USE_JMONKEY_
6481  #bpy.utils.register_module(__name__) ## do not load all the ogre panels by default
6482  #_header_ = bpy.types.INFO_HT_header
6483  #bpy.utils.unregister_class(_header_)
6484  restore_minimal_interface()
6485 
6486  # only test for Tundra2 once - do not do this every panel redraw ##
6487  if os.path.isdir( CONFIG['TUNDRA_ROOT'] ): _USE_TUNDRA_ = True
6488  else: _USE_TUNDRA_ = False
6489  #if os.path.isdir( CONFIG['JMONKEY_ROOT'] ): _USE_JMONKEY_ = True
6490  #else: _USE_JMONKEY_ = False
6491 
6492  bpy.types.INFO_MT_file_export.append(export_menu_func_ogre)
6493  bpy.types.INFO_MT_file_export.append(export_menu_func_realxtend)
6494 
6495  bpy.utils.register_class(PopUpDialogOperator)
6496 
6497  if os.path.isdir( CONFIG['USER_MATERIALS'] ):
6498  scripts,progs = update_parent_material_path( CONFIG['USER_MATERIALS'] )
6499  for prog in progs:
6500  print('Ogre shader program', prog.name)
6501  else:
6502  print('[WARNING]: Invalid my-shaders path %s' % CONFIG['USER_MATERIALS'])
6503 
6504 def unregister():
6505  print('Unloading blender2ogre', VERSION)
6506  bpy.utils.unregister_module(__name__)
6507  try: bpy.utils.register_class(_header_)
6508  except: pass
6509 
6510  # If the addon is disabled while the UI is toggled, reset it for next time.
6511  # "Untoggling" it by setting the value to True seems a bit counter-intuitive.
6512  OgreToggleInterfaceOp.TOGGLE = True
6513  bpy.types.INFO_MT_file_export.remove(export_menu_func_ogre)
6514  bpy.types.INFO_MT_file_export.remove(export_menu_func_realxtend)
6515  # This seems to be not registered by the time this function is called.
6516  #bpy.utils.unregister_class(PopUpDialogOperator)
6517 
6518 
6519 ## Blender world panel options for EC_SkyX creation
6520 ## todo: EC_SkyX has changes a bit lately, see that
6521 ## all these options are still correct and valid
6522 ## old todo (?): Move to tundra.py
6523 
6524 bpy.types.World.ogre_skyX = BoolProperty(
6525  name="enable sky", description="ogre sky",
6526  default=False
6527 )
6528 bpy.types.World.ogre_skyX_time = FloatProperty(
6529  name="Time Multiplier",
6530  description="change speed of day/night cycle",
6531  default=0.3,
6532  min=0.0, max=5.0
6533 )
6534 bpy.types.World.ogre_skyX_wind = FloatProperty(
6535  name="Wind Direction",
6536  description="change direction of wind",
6537  default=33.0,
6538  min=0.0, max=360.0
6539 )
6540 bpy.types.World.ogre_skyX_volumetric_clouds = BoolProperty(
6541  name="volumetric clouds", description="toggle ogre volumetric clouds",
6542  default=True
6543 )
6544 bpy.types.World.ogre_skyX_cloud_density_x = FloatProperty(
6545  name="Cloud Density X",
6546  description="change density of volumetric clouds on X",
6547  default=0.1,
6548  min=0.0, max=5.0
6549 )
6550 bpy.types.World.ogre_skyX_cloud_density_y = FloatProperty(
6551  name="Cloud Density Y",
6552  description="change density of volumetric clouds on Y",
6553  default=1.0,
6554  min=0.0, max=5.0
6555 )
6556 
6557 ## Sky UI panel
6558 
6559 @UI
6560 class OgreSkyPanel(bpy.types.Panel):
6561  bl_space_type = 'PROPERTIES'
6562  bl_region_type = 'WINDOW'
6563  bl_context = "world"
6564  bl_label = "Ogre Sky Settings"
6565 
6566  @classmethod
6567  def poll(cls, context):
6568  return True
6569 
6570  def draw(self, context):
6571  layout = self.layout
6572  box = layout.box()
6573  box.prop( context.world, 'ogre_skyX' )
6574  if context.world.ogre_skyX:
6575  box.prop( context.world, 'ogre_skyX_time' )
6576  box.prop( context.world, 'ogre_skyX_wind' )
6577  box.prop( context.world, 'ogre_skyX_volumetric_clouds' )
6578  if context.world.ogre_skyX_volumetric_clouds:
6579  box.prop( context.world, 'ogre_skyX_cloud_density_x' )
6580  box.prop( context.world, 'ogre_skyX_cloud_density_y' )
6581 
6582 
6583 class OgreProgram(object):
6584  '''
6585  parses .program scripts
6586  saves bytes to copy later
6587 
6588  self.name = name of program reference
6589  self.source = name of shader program (.cg, .glsl)
6590  '''
6591 
6592  def save( self, path ):
6593  print('saving program to', path)
6594  f = open( os.path.join(path,self.source), 'wb' )
6595  f.write(self.source_bytes )
6596  f.close()
6597  for name in self.includes:
6598  f = open( os.path.join(path,name), 'wb' )
6599  f.write( self.includes[name] )
6600  f.close()
6601 
6602  PROGRAMS = {}
6603 
6604  def reload(self): # only one directory is allowed to hold shader programs
6605  if self.source not in os.listdir( CONFIG['SHADER_PROGRAMS'] ):
6606  print( 'ERROR: ogre material %s is missing source: %s' %(self.name,self.source) )
6607  print( CONFIG['SHADER_PROGRAMS'] )
6608  return False
6609  url = os.path.join( CONFIG['SHADER_PROGRAMS'], self.source )
6610  print('shader source:', url)
6611  self.source_bytes = open( url, 'rb' ).read()#.decode('utf-8')
6612  print('shader source num bytes:', len(self.source_bytes))
6613  data = self.source_bytes.decode('utf-8')
6614 
6615  for line in data.splitlines(): # only cg shaders use the include macro?
6616  if line.startswith('#include') and line.count('"')==2:
6617  name = line.split()[-1].replace('"','').strip()
6618  print('shader includes:', name)
6619  url = os.path.join( CONFIG['SHADER_PROGRAMS'], name )
6620  self.includes[ name ] = open( url, 'rb' ).read()
6621  return True
6622 
6623  def __init__(self, name='', data=''):
6624  self.name=name
6625  self.data = data.strip()
6626  self.source = None
6627  self.includes = {} # cg files may use #include something.cg
6628 
6629  if self.name in OgreProgram.PROGRAMS:
6630  print('---copy ogreprogram---', self.name)
6631  other = OgreProgram.PROGRAMS
6632  self.source = other.source
6633  self.data = other.data
6634  self.entry_point = other.entry_point
6635  self.profiles = other.profiles
6636 
6637  if data: self.parse( self.data )
6638  if self.name: OgreProgram.PROGRAMS[ self.name ] = self
6639 
6640  def parse( self, txt ):
6641  self.data = txt
6642  print('--parsing ogre shader program--' )
6643  for line in self.data.splitlines():
6644  print(line)
6645  line = line.split('//')[0]
6646  line = line.strip()
6647  if line.startswith('vertex_program') or line.startswith('fragment_program'):
6648  a, self.name, self.type = line.split()
6649 
6650  elif line.startswith('source'): self.source = line.split()[-1]
6651  elif line.startswith('entry_point'): self.entry_point = line.split()[-1]
6652  elif line.startswith('profiles'): self.profiles = line.split()[1:]
6653 
6654 ## Ogre Material object(s) that is utilized during export stages
6655 
6656 class OgreMaterialScript(object):
6657  def get_programs(self):
6658  progs = []
6659  for name in list(self.vertex_programs.keys()) + list(self.fragment_programs.keys()):
6660  p = get_shader_program( name ) # OgreProgram.PROGRAMS
6661  if p: progs.append( p )
6662  return progs
6663 
6664  def __init__(self, txt, url):
6665  self.url = url
6666  self.data = txt.strip()
6667  self.parent = None
6668  self.vertex_programs = {}
6669  self.fragment_programs = {}
6670  self.texture_units = {}
6671  self.texture_units_order = []
6672  self.passes = []
6673 
6674  line = self.data.splitlines()[0]
6675  assert line.startswith('material')
6676  if ':' in line:
6677  line, self.parent = line.split(':')
6678  self.name = line.split()[-1]
6679  print( 'new ogre material: %s' %self.name )
6680 
6681  brace = 0
6682  self.techniques = techs = []
6683  prog = None # pick up program params
6684  tex = None # pick up texture_unit options, require "texture" ?
6685  for line in self.data.splitlines():
6686  #print( line )
6687  rawline = line
6688  line = line.split('//')[0]
6689  line = line.strip()
6690  if not line: continue
6691 
6692  if line == '{': brace += 1
6693  elif line == '}': brace -= 1; prog = None; tex = None
6694 
6695  if line.startswith( 'technique' ):
6696  tech = {'passes':[]}; techs.append( tech )
6697  if len(line.split()) > 1: tech['technique-name'] = line.split()[-1]
6698  elif techs:
6699  if line.startswith('pass'):
6700  P = {'texture_units':[], 'vprogram':None, 'fprogram':None, 'body':[]}
6701  tech['passes'].append( P )
6702  self.passes.append( P )
6703 
6704  elif tech['passes']:
6705  P = tech['passes'][-1]
6706  P['body'].append( rawline )
6707 
6708  if line == '{' or line == '}': continue
6709 
6710  if line.startswith('vertex_program_ref'):
6711  prog = P['vprogram'] = {'name':line.split()[-1], 'params':{}}
6712  self.vertex_programs[ prog['name'] ] = prog
6713  elif line.startswith('fragment_program_ref'):
6714  prog = P['fprogram'] = {'name':line.split()[-1], 'params':{}}
6715  self.fragment_programs[ prog['name'] ] = prog
6716 
6717  elif line.startswith('texture_unit'):
6718  prog = None
6719  tex = {'name':line.split()[-1], 'params':{}}
6720  if tex['name'] == 'texture_unit': # ignore unnamed texture units
6721  print('WARNING: material %s contains unnamed texture_units' %self.name)
6722  print('---unnamed texture units will be ignored---')
6723  else:
6724  P['texture_units'].append( tex )
6725  self.texture_units[ tex['name'] ] = tex
6726  self.texture_units_order.append( tex['name'] )
6727 
6728  elif prog:
6729  p = line.split()[0]
6730  if p=='param_named':
6731  items = line.split()
6732  if len(items) == 4: p, o, t, v = items
6733  elif len(items) == 3:
6734  p, o, v = items
6735  t = 'class'
6736  elif len(items) > 4:
6737  o = items[1]; t = items[2]
6738  v = items[3:]
6739 
6740  opt = { 'name': o, 'type':t, 'raw_value':v }
6741  prog['params'][ o ] = opt
6742  if t=='float': opt['value'] = float(v)
6743  elif t in 'float2 float3 float4'.split(): opt['value'] = [ float(a) for a in v ]
6744  else: print('unknown type:', t)
6745 
6746  elif tex: # (not used)
6747  tex['params'][ line.split()[0] ] = line.split()[ 1 : ]
6748 
6749  for P in self.passes:
6750  lines = P['body']
6751  while lines and ''.join(lines).count('{')!=''.join(lines).count('}'):
6752  if lines[-1].strip() == '}': lines.pop()
6753  else: break
6754  P['body'] = '\n'.join( lines )
6755  assert P['body'].count('{') == P['body'].count('}') # if this fails, the parser choked
6756 
6757  #print( self.techniques )
6758  self.hidden_texture_units = rem = []
6759  for tex in self.texture_units.values():
6760  if 'texture' not in tex['params']:
6761  rem.append( tex )
6762  for tex in rem:
6763  print('WARNING: not using texture_unit because it lacks a "texture" parameter', tex['name'])
6764  self.texture_units.pop( tex['name'] )
6765 
6766  if len(self.techniques)>1:
6767  print('WARNING: user material %s has more than one technique' %self.url)
6768 
6769  def as_abstract_passes( self ):
6770  r = []
6771  for i,P in enumerate(self.passes):
6772  head = 'abstract pass %s/PASS%s' %(self.name,i)
6773  r.append( head + '\n' + P['body'] )
6774  return r
6775 
6776 class MaterialScripts(object):
6777  ALL_MATERIALS = {}
6778  ENUM_ITEMS = []
6779 
6780  def __init__(self, url):
6781  self.url = url
6782  self.data = ''
6783  data = open( url, 'rb' ).read()
6784  try:
6785  self.data = data.decode('utf-8')
6786  except:
6787  self.data = data.decode('latin-1')
6788 
6789  self.materials = {}
6790  ## chop up .material file, find all material defs ####
6791  mats = []
6792  mat = []
6793  skip = False # for now - programs must be defined in .program files, not in the .material
6794  for line in self.data.splitlines():
6795  if not line.strip(): continue
6796  a = line.split()[0] #NOTE ".split()" strips white space
6797  if a == 'material':
6798  mat = []; mats.append( mat )
6799  mat.append( line )
6800  elif a in ('vertex_program', 'fragment_program', 'abstract'):
6801  skip = True
6802  elif mat and not skip:
6803  mat.append( line )
6804  elif skip and line=='}':
6805  skip = False
6806 
6807  ##########################
6808  for mat in mats:
6809  omat = OgreMaterialScript( '\n'.join( mat ), url )
6810  if omat.name in self.ALL_MATERIALS:
6811  print( 'WARNING: material %s redefined' %omat.name )
6812  #print( '--OLD MATERIAL--')
6813  #print( self.ALL_MATERIALS[ omat.name ].data )
6814  #print( '--NEW MATERIAL--')
6815  #print( omat.data )
6816  self.materials[ omat.name ] = omat
6817  self.ALL_MATERIALS[ omat.name ] = omat
6818  if omat.vertex_programs or omat.fragment_programs: # ignore materials without programs
6819  self.ENUM_ITEMS.append( (omat.name, omat.name, url) )
6820 
6821  @classmethod # only call after parsing all material scripts
6822  def reset_rna(self, callback=None):
6823  bpy.types.Material.ogre_parent_material = EnumProperty(
6824  name="Script Inheritence",
6825  description='ogre parent material class',
6826  items=self.ENUM_ITEMS,
6827  #update=callback
6828  )
6829 
6830 ## Image/texture proecssing
6831 
6832 def is_image_postprocessed( image ):
6833  if CONFIG['FORCE_IMAGE_FORMAT'] != 'NONE' or image.use_resize_half or image.use_resize_absolute or image.use_color_quantize or image.use_convert_format:
6834  return True
6835  else:
6836  return False
6837 
6838 class _image_processing_( object ):
6839  def _reformat( self, name, image ):
6840  if image.convert_format != 'NONE':
6841  name = '%s.%s' %(name[:name.rindex('.')], image.convert_format)
6842  if image.convert_format == 'dds': name = '_DDS_.%s' %name
6843  elif image.use_resize_half or image.use_resize_absolute or image.use_color_quantize or image.use_convert_format:
6844  name = '_magick_.%s' %name
6845  if CONFIG['FORCE_IMAGE_FORMAT'] != 'NONE' and not name.endswith('.dds'):
6846  name = '%s.%s' %(name[:name.rindex('.')], CONFIG['FORCE_IMAGE_FORMAT'])
6847  if CONFIG['FORCE_IMAGE_FORMAT'] == 'dds':
6848  name = '_DDS_.%s' %name
6849  return name
6850 
6851  def image_magick( self, texture, infile ):
6852  print('IMAGE MAGICK', infile )
6853  exe = CONFIG['IMAGE_MAGICK_CONVERT']
6854  if not os.path.isfile( exe ):
6855  Report.warnings.append( 'ImageMagick not installed!' )
6856  print( 'ERROR: can not find Image Magick - convert', exe ); return
6857  cmd = [ exe, infile ]
6858  ## enforce max size ##
6859  x,y = texture.image.size
6860  ax = texture.image.resize_x
6861  ay = texture.image.resize_y
6862 
6863  if texture.image.use_convert_format and texture.image.convert_format == 'jpg':
6864  cmd.append( '-quality' )
6865  cmd.append( '%s' %texture.image.jpeg_quality )
6866 
6867  if texture.image.use_resize_half:
6868  cmd.append( '-resize' )
6869  cmd.append( '%sx%s' %(x/2, y/2) )
6870  elif texture.image.use_resize_absolute and (x>ax or y>ay):
6871  cmd.append( '-resize' )
6872  cmd.append( '%sx%s' %(ax,ay) )
6873 
6874  elif x > CONFIG['MAX_TEXTURE_SIZE'] or y > CONFIG['MAX_TEXTURE_SIZE']:
6875  cmd.append( '-resize' )
6876  cmd.append( str(CONFIG['MAX_TEXTURE_SIZE']) )
6877 
6878  if texture.image.use_color_quantize:
6879  if texture.image.use_color_quantize_dither:
6880  cmd.append( '+dither' )
6881  cmd.append( '-colors' )
6882  cmd.append( str(texture.image.color_quantize) )
6883 
6884  path,name = os.path.split( infile )
6885  #if (texture.image.use_convert_format and texture.image.convert_format == 'dds') or CONFIG['FORCE_IMAGE_FORMAT'] == 'dds':
6886  outfile = os.path.join( path, self._reformat(name,texture.image) )
6887  if outfile.endswith('.dds'):
6888  temp = os.path.join( path, '_temp_.png' )
6889  cmd.append( temp )
6890  print( 'IMAGE MAGICK: %s' %cmd )
6891  subprocess.call( cmd )
6892  self.nvcompress( texture, temp, outfile=outfile )
6893 
6894  else:
6895  cmd.append( outfile )
6896  print( 'IMAGE MAGICK: %s' %cmd )
6897  subprocess.call( cmd )
6898 
6899  def nvcompress(self, texture, infile, outfile=None, version=1, fast=False, blocking=True):
6900  print('[NVCompress DDS Wrapper]', infile )
6901  assert version in (1,2,3,4,5)
6902  exe = CONFIG['NVCOMPRESS']
6903  cmd = [ exe ]
6904 
6905  if texture.image.use_alpha and texture.image.depth==32:
6906  cmd.append( '-alpha' )
6907  if not texture.use_mipmap:
6908  cmd.append( '-nomips' )
6909 
6910  if texture.use_normal_map:
6911  cmd.append( '-normal' )
6912  if version in (1,3):
6913  cmd.append( '-bc%sn' %version )
6914  else:
6915  cmd.append( '-bc%s' %version )
6916  else:
6917  cmd.append( '-bc%s' %version )
6918 
6919  if fast:
6920  cmd.append( '-fast' )
6921  cmd.append( infile )
6922 
6923  if outfile: cmd.append( outfile )
6924 
6925  print( cmd )
6926  if blocking:
6927  subprocess.call( cmd )
6928  else:
6929  subprocess.Popen( cmd )
6930 
6931 ## NVIDIA texture compress documentation
6932 
6933 _nvcompress_doc = '''
6934 usage: nvcompress [options] infile [outfile]
6935 
6936 Input options:
6937  -color The input image is a color map (default).
6938  -alpha The input image has an alpha channel used for transparency.
6939  -normal The input image is a normal map.
6940  -tonormal Convert input to normal map.
6941  -clamp Clamp wrapping mode (default).
6942  -repeat Repeat wrapping mode.
6943  -nomips Disable mipmap generation.
6944 
6945 Compression options:
6946  -fast Fast compression.
6947  -nocuda Do not use cuda compressor.
6948  -rgb RGBA format
6949  -bc1 BC1 format (DXT1)
6950  -bc1n BC1 normal map format (DXT1nm)
6951  -bc1a BC1 format with binary alpha (DXT1a)
6952  -bc2 BC2 format (DXT3)
6953  -bc3 BC3 format (DXT5)
6954  -bc3n BC3 normal map format (DXT5nm)
6955  -bc4 BC4 format (ATI1)
6956  -bc5 BC5 format (3Dc/ATI2)
6957 '''
6958 
6959 class OgreMaterialGenerator( _image_processing_ ):
6960  def __init__(self, material, path='/tmp', touch_textures=False ):
6961  self.material = material # top level material
6962  self.path = path # copy textures to path
6963  self.passes = []
6964  self.touch_textures = touch_textures
6965  if material.node_tree:
6966  nodes = bpyShaders.get_subnodes( self.material.node_tree, type='MATERIAL_EXT' )
6967  for node in nodes:
6968  if node.material:
6969  self.passes.append( node.material )
6970 
6971  def get_active_programs(self):
6972  r = []
6973  for mat in self.passes:
6974  if mat.use_ogre_parent_material and mat.ogre_parent_material:
6975  usermat = get_ogre_user_material( mat.ogre_parent_material )
6976  for prog in usermat.get_programs(): r.append( prog )
6977  return r
6978 
6979  def get_header(self):
6980  r = []
6981  for mat in self.passes:
6982  if mat.use_ogre_parent_material and mat.ogre_parent_material:
6983  usermat = get_ogre_user_material( mat.ogre_parent_material )
6984  r.append( '// user material: %s' %usermat.name )
6985  for prog in usermat.get_programs():
6986  r.append( prog.data )
6987  r.append( '// abstract passes //' )
6988  r += usermat.as_abstract_passes()
6989  return '\n'.join( r )
6990 
6991  def get_passes(self):
6992  r = []
6993  r.append( self.generate_pass(self.material) )
6994  for mat in self.passes:
6995  if mat.use_in_ogre_material_pass: # submaterials
6996  r.append( self.generate_pass(mat) )
6997  return r
6998 
6999  def generate_pass( self, mat, pass_name=None ):
7000  usermat = texnodes = None
7001  if mat.use_ogre_parent_material and mat.ogre_parent_material:
7002  usermat = get_ogre_user_material( mat.ogre_parent_material )
7003  texnodes = bpyShaders.get_texture_subnodes( self.material, mat )
7004 
7005  M = ''
7006  if not pass_name: pass_name = mat.name
7007  if usermat:
7008  M += indent(2, 'pass %s : %s/PASS0' %(pass_name,usermat.name), '{' )
7009  else:
7010  M += indent(2, 'pass %s'%pass_name, '{' )
7011 
7012  color = mat.diffuse_color
7013  alpha = 1.0
7014  if mat.use_transparency:
7015  alpha = mat.alpha
7016 
7017  slots = get_image_textures( mat ) # returns texture_slot objects (CLASSIC MATERIAL)
7018  usealpha = False #mat.ogre_depth_write
7019  for slot in slots:
7020  #if slot.use_map_alpha and slot.texture.use_alpha: usealpha = True; break
7021  if (slot.texture.image is not None) and (slot.texture.image.use_alpha): usealpha = True; break
7022 
7023  ## force material alpha to 1.0 if textures use_alpha?
7024  #if usealpha: alpha = 1.0 # let the alpha of the texture control material alpha?
7025 
7026  if mat.use_fixed_pipeline:
7027  f = mat.ambient
7028  if mat.use_vertex_color_paint:
7029  M += indent(3, 'ambient vertexcolour' )
7030  else: # fall back to basic material
7031  M += indent(3, 'ambient %s %s %s %s' %(color.r*f, color.g*f, color.b*f, alpha) )
7032 
7033  f = mat.diffuse_intensity
7034  if mat.use_vertex_color_paint:
7035  M += indent(3, 'diffuse vertexcolour' )
7036  else: # fall back to basic material
7037  M += indent(3, 'diffuse %s %s %s %s' %(color.r*f, color.g*f, color.b*f, alpha) )
7038 
7039  f = mat.specular_intensity
7040  s = mat.specular_color
7041  M += indent(3, 'specular %s %s %s %s %s' %(s.r*f, s.g*f, s.b*f, alpha, mat.specular_hardness/4.0) )
7042 
7043  f = mat.emit
7044  if mat.use_shadeless: # requested by Borris
7045  M += indent(3, 'emissive %s %s %s 1.0' %(color.r, color.g, color.b) )
7046  elif mat.use_vertex_color_light:
7047  M += indent(3, 'emissive vertexcolour' )
7048  else:
7049  M += indent(3, 'emissive %s %s %s %s' %(color.r*f, color.g*f, color.b*f, alpha) )
7050  M += '\n' # pretty printing
7051 
7052  if mat.offset_z:
7053  M += indent(3, 'depth_bias %s'%mat.offset_z )
7054 
7055  for name in dir(mat): #mat.items() - items returns custom props not pyRNA:
7056  if name.startswith('ogre_') and name != 'ogre_parent_material':
7057  var = getattr(mat,name)
7058  op = name.replace('ogre_', '')
7059  val = var
7060  if type(var) == bool:
7061  if var: val = 'on'
7062  else: val = 'off'
7063  M += indent( 3, '%s %s' %(op,val) )
7064  M += '\n' # pretty printing
7065 
7066  if texnodes and usermat.texture_units:
7067  for i,name in enumerate(usermat.texture_units_order):
7068  if i<len(texnodes):
7069  node = texnodes[i]
7070  if node.texture:
7071  geo = bpyShaders.get_connected_input_nodes( self.material, node )[0]
7072  M += self.generate_texture_unit( node.texture, name=name, uv_layer=geo.uv_layer )
7073  elif slots:
7074  for slot in slots:
7075  M += self.generate_texture_unit( slot.texture, slot=slot )
7076 
7077  M += indent(2, '}' ) # end pass
7078  return M
7079 
7080  def generate_texture_unit(self, texture, slot=None, name=None, uv_layer=None):
7081  if not hasattr(texture, 'image'):
7082  print('WARNING: texture must be of type IMAGE->', texture)
7083  return ''
7084  if not texture.image:
7085  print('WARNING: texture has no image assigned->', texture)
7086  return ''
7087  #if slot: print(dir(slot))
7088  if slot and not slot.use: return ''
7089 
7090  path = self.path #CONFIG['PATH']
7091 
7092  M = ''; _alphahack = None
7093  if not name: name = '' #texture.name # (its unsafe to use texture block name)
7094  M += indent(3, 'texture_unit %s' %name, '{' )
7095 
7096  if texture.library: # support library linked textures
7097  libpath = os.path.split( bpy.path.abspath(texture.library.filepath) )[0]
7098  iurl = bpy.path.abspath( texture.image.filepath, libpath )
7099  else:
7100  iurl = bpy.path.abspath( texture.image.filepath )
7101 
7102  postname = texname = os.path.split(iurl)[-1]
7103  destpath = path
7104 
7105  if texture.image.packed_file:
7106  orig = texture.image.filepath
7107  iurl = os.path.join(path, texname)
7108  if '.' not in iurl:
7109  print('WARNING: packed image is of unknown type - assuming PNG format')
7110  iurl += '.png'
7111  texname = postname = os.path.split(iurl)[-1]
7112 
7113  if not os.path.isfile( iurl ):
7114  if self.touch_textures:
7115  print('MESSAGE: unpacking image: ', iurl)
7116  texture.image.filepath = iurl
7117  texture.image.save()
7118  texture.image.filepath = orig
7119  else:
7120  print('MESSAGE: packed image already in temp, not updating', iurl)
7121 
7122  if is_image_postprocessed( texture.image ):
7123  postname = self._reformat( texname, texture.image )
7124  print('MESSAGE: image postproc',postname)
7125 
7126  M += indent(4, 'texture %s' %postname )
7127 
7128  exmode = texture.extension
7129  if exmode in TextureUnit.tex_address_mode:
7130  M += indent(4, 'tex_address_mode %s' %TextureUnit.tex_address_mode[exmode] )
7131 
7132 
7133  # TODO - hijack nodes for better control?
7134  if slot: # classic blender material slot options
7135  if exmode == 'CLIP': M += indent(4, 'tex_border_colour %s %s %s' %(slot.color.r, slot.color.g, slot.color.b) )
7136  M += indent(4, 'scale %s %s' %(1.0/slot.scale.x, 1.0/slot.scale.y) )
7137  if slot.texture_coords == 'REFLECTION':
7138  if slot.mapping == 'SPHERE':
7139  M += indent(4, 'env_map spherical' )
7140  elif slot.mapping == 'FLAT':
7141  M += indent(4, 'env_map planar' )
7142  else: print('WARNING: <%s> has a non-UV mapping type (%s) and not picked a proper projection type of: Sphere or Flat' %(texture.name, slot.mapping))
7143 
7144  ox,oy,oz = slot.offset
7145  if ox or oy:
7146  M += indent(4, 'scroll %s %s' %(ox,oy) )
7147  if oz:
7148  M += indent(4, 'rotate %s' %oz )
7149 
7150  #if slot.use_map_emission: # problem, user will want to use emission sometimes
7151  if slot.use_from_dupli: # hijacked again - june7th
7152  M += indent(4, 'rotate_anim %s' %slot.emission_color_factor )
7153  if slot.use_map_scatter: # hijacked from volume shaders
7154  M += indent(4, 'scroll_anim %s %s ' %(slot.density_factor, slot.emission_factor) )
7155 
7156  if slot.uv_layer:
7157  idx = find_uv_layer_index( slot.uv_layer, self.material )
7158  M += indent(4, 'tex_coord_set %s' %idx)
7159 
7160  rgba = False
7161  if texture.image.depth == 32: rgba = True
7162  btype = slot.blend_type # TODO - fix this hack if/when slots support pyRNA
7163  ex = False; texop = None
7164  if btype in TextureUnit.colour_op:
7165  if btype=='MIX' and slot.use_map_alpha and not slot.use_stencil:
7166  if slot.diffuse_color_factor >= 1.0: texop = 'alpha_blend'
7167  else:
7168  texop = TextureUnit.colour_op[ btype ]
7169  ex = True
7170  elif btype=='MIX' and slot.use_map_alpha and slot.use_stencil:
7171  texop = 'blend_current_alpha'; ex=True
7172  elif btype=='MIX' and not slot.use_map_alpha and slot.use_stencil:
7173  texop = 'blend_texture_alpha'; ex=True
7174  else:
7175  texop = TextureUnit.colour_op[ btype ]
7176  elif btype in TextureUnit.colour_op_ex:
7177  texop = TextureUnit.colour_op_ex[ btype ]
7178  ex = True
7179 
7180  if texop and ex:
7181  if texop == 'blend_manual':
7182  factor = 1.0 - slot.diffuse_color_factor
7183  M += indent(4, 'colour_op_ex %s src_texture src_current %s' %(texop, factor) )
7184  else:
7185  M += indent(4, 'colour_op_ex %s src_texture src_current' %texop )
7186  elif texop:
7187  M += indent(4, 'colour_op %s' %texop )
7188 
7189  else:
7190  if uv_layer:
7191  idx = find_uv_layer_index( uv_layer )
7192  M += indent(4, 'tex_coord_set %s' %idx)
7193 
7194  M += indent(3, '}' )
7195 
7196  if self.touch_textures:
7197  # Copy texture only if newer
7198  if not os.path.isfile(iurl):
7199  Report.warnings.append('Missing texture: %s' %iurl )
7200  else:
7201  desturl = os.path.join( destpath, texname )
7202  updated = False
7203  if not os.path.isfile( desturl ) or os.stat( desturl ).st_mtime < os.stat( iurl ).st_mtime:
7204  f = open( desturl, 'wb' )
7205  f.write( open(iurl,'rb').read() )
7206  f.close()
7207  updated = True
7208  posturl = os.path.join(destpath,postname)
7209  if is_image_postprocessed( texture.image ):
7210  if not os.path.isfile( posturl ) or updated:
7211  self.image_magick( texture, desturl ) # calls nvconvert if required
7212 
7213  return M
7214 
7215 class TextureUnit(object):
7216  colour_op = {
7217  'MIX' : 'modulate', # Ogre Default - was "replace" but that kills lighting
7218  'ADD' : 'add',
7219  'MULTIPLY' : 'modulate',
7220  #'alpha_blend' : '',
7221  }
7222  colour_op_ex = {
7223  'MIX' : 'blend_manual',
7224  'SCREEN': 'modulate_x2',
7225  'LIGHTEN': 'modulate_x4',
7226  'SUBTRACT': 'subtract',
7227  'OVERLAY': 'add_signed',
7228  'DIFFERENCE': 'dotproduct', # best match?
7229  'VALUE': 'blend_diffuse_colour',
7230  }
7231 
7232  tex_address_mode = {
7233  'REPEAT': 'wrap',
7234  'EXTEND': 'clamp',
7235  'CLIP' : 'border',
7236  'CHECKER' : 'mirror'
7237  }
7238 
7239 
7240 @UI
7241 class PANEL_Object(bpy.types.Panel):
7242  bl_space_type = 'PROPERTIES'
7243  bl_region_type = 'WINDOW'
7244  bl_context = "object"
7245  bl_label = "Object+"
7246 
7247  @classmethod
7248  def poll(cls, context):
7249  if _USE_TUNDRA_ and context.active_object:
7250  return True
7251 
7252  def draw(self, context):
7253  ob = context.active_object
7254  layout = self.layout
7255  box = layout.box()
7256  box.prop( ob, 'cast_shadows' )
7257 
7258  box.prop( ob, 'use_draw_distance' )
7259  if ob.use_draw_distance:
7260  box.prop( ob, 'draw_distance' )
7261  #if ob.find_armature():
7262  if ob.type == 'EMPTY':
7263  box.prop( ob, 'use_avatar' )
7264  box.prop( ob, 'avatar_reference' )
7265 
7266 @UI
7267 class PANEL_Speaker(bpy.types.Panel):
7268  bl_space_type = 'PROPERTIES'
7269  bl_region_type = 'WINDOW'
7270  bl_context = "data"
7271  bl_label = "Sound+"
7272  @classmethod
7273  def poll(cls, context):
7274  if context.active_object and context.active_object.type=='SPEAKER': return True
7275  def draw(self, context):
7276  layout = self.layout
7277  box = layout.box()
7278  box.prop( context.active_object.data, 'play_on_load' )
7279  box.prop( context.active_object.data, 'loop' )
7280  box.prop( context.active_object.data, 'use_spatial' )
7281 
7282 @UI
7283 class PANEL_MultiResLOD(bpy.types.Panel):
7284  bl_space_type = 'PROPERTIES'
7285  bl_region_type = 'WINDOW'
7286  bl_context = "modifier"
7287  bl_label = "Multi-Resolution LOD"
7288  @classmethod
7289  def poll(cls, context):
7290  if context.active_object and context.active_object.type=='MESH':
7291  ob = context.active_object
7292  if ob.modifiers and ob.modifiers[0].type=='MULTIRES':
7293  return True
7294  def draw(self, context):
7295  ob = context.active_object
7296  layout = self.layout
7297  box = layout.box()
7298  box.prop( ob, 'use_multires_lod' )
7299  if ob.use_multires_lod:
7300  box.prop( ob, 'multires_lod_range' )
7301 
7302 ## Public API (continued)
7303 
7304 def material_name( mat, clean = False ):
7305  if type(mat) is str:
7306  return mat
7307  elif not mat.library:
7308  return mat.name
7309  else:
7310  if clean:
7311  return clean_object_name(mat.name + mat.library.filepath.replace('/','_'))
7312 
7313 def export_mesh(ob, path='/tmp', force_name=None, ignore_shape_animation=False, normals=True):
7314  ''' returns materials used by the mesh '''
7315  return dot_mesh( ob, path, force_name, ignore_shape_animation, normals )
7316 
7317 def generate_material(mat, path='/tmp', copy_programs=False, touch_textures=False):
7318  ''' returns generated material string '''
7319 
7320  safename = material_name(mat) # supports blender library linking
7321  M = '// %s generated by blender2ogre %s\n\n' % (mat.name, VERSION)
7322 
7323  M += 'material %s \n{\n' % safename # start material
7324  if mat.use_shadows:
7325  M += indent(1, 'receive_shadows on \n')
7326  else:
7327  M += indent(1, 'receive_shadows off \n')
7328 
7329  M += indent(1, 'technique', '{' ) # technique GLSL, CG
7330  w = OgreMaterialGenerator(mat, path=path, touch_textures=touch_textures)
7331 
7332  if copy_programs:
7333  progs = w.get_active_programs()
7334  for prog in progs:
7335  if prog.source:
7336  prog.save(path)
7337  else:
7338  print( '[WARNING}: material %s uses program %s which has no source' % (mat.name, prog.name) )
7339 
7340  header = w.get_header()
7341  passes = w.get_passes()
7342 
7343  M += '\n'.join(passes)
7344  M += indent(1, '}' ) # end technique
7345  M += '}\n' # end material
7346 
7347  if len(header) > 0:
7348  return header + '\n' + M
7349  else:
7350  return M
7351 
7352 def get_ogre_user_material( name ):
7353  if name in MaterialScripts.ALL_MATERIALS:
7354  return MaterialScripts.ALL_MATERIALS[ name ]
7355 
7356 def get_shader_program( name ):
7357  if name in OgreProgram.PROGRAMS:
7358  return OgreProgram.PROGRAMS[ name ]
7359  else:
7360  print('WARNING: no shader program named: %s' %name)
7361 
7362 def get_shader_programs():
7363  return OgreProgram.PROGRAMS.values()
7364 
7365 def parse_material_and_program_scripts( path, scripts, progs, missing ): # recursive
7366  for name in os.listdir(path):
7367  url = os.path.join(path,name)
7368  if os.path.isdir( url ):
7369  parse_material_and_program_scripts( url, scripts, progs, missing )
7370 
7371  elif os.path.isfile( url ):
7372  if name.endswith( '.material' ):
7373  print( '<found material>', url )
7374  scripts.append( MaterialScripts( url ) )
7375 
7376  if name.endswith('.program'):
7377  print( '<found program>', url )
7378  data = open( url, 'rb' ).read().decode('utf-8')
7379 
7380  chk = []; chunks = [ chk ]
7381  for line in data.splitlines():
7382  line = line.split('//')[0]
7383  if line.startswith('}'):
7384  chk.append( line )
7385  chk = []; chunks.append( chk )
7386  elif line.strip():
7387  chk.append( line )
7388 
7389  for chk in chunks:
7390  if not chk: continue
7391  p = OgreProgram( data='\n'.join(chk) )
7392  if p.source:
7393  ok = p.reload()
7394  if not ok: missing.append( p )
7395  else: progs.append( p )
7396 
7397 def update_parent_material_path( path ):
7398  ''' updates RNA '''
7399  print( '>>SEARCHING FOR OGRE MATERIALS: %s' %path )
7400  scripts = []
7401  progs = []
7402  missing = []
7403  parse_material_and_program_scripts( path, scripts, progs, missing )
7404 
7405  if missing:
7406  print('WARNING: missing shader programs:')
7407  for p in missing: print(p.name)
7408  if missing and not progs:
7409  print('WARNING: no shader programs were found - set "SHADER_PROGRAMS" to your path')
7410 
7411  MaterialScripts.reset_rna( callback=bpyShaders.on_change_parent_material )
7412  return scripts, progs
7413 
7414 def get_subcollision_meshes():
7415  ''' returns all collision meshes found in the scene '''
7416  r = []
7417  for ob in bpy.context.scene.objects:
7418  if ob.type=='MESH' and ob.subcollision: r.append( ob )
7419  return r
7420 
7421 def get_objects_with_subcollision():
7422  ''' returns objects that have active sub-collisions '''
7423  r = []
7424  for ob in bpy.context.scene.objects:
7425  if ob.type=='MESH' and ob.collision_mode not in ('NONE', 'PRIMITIVE'):
7426  r.append( ob )
7427  return r
7428 
7429 def get_subcollisions(ob):
7430  prefix = '%s.' %ob.collision_mode
7431  r = []
7432  for child in ob.children:
7433  if child.subcollision and child.name.startswith( prefix ):
7434  r.append( child )
7435  return r
7436 
7437 class bpyShaders(bpy.types.Operator):
7438  '''operator: enables material nodes (workaround for not having IDPointers in pyRNA)'''
7439  bl_idname = "ogre.force_setup_material_passes"
7440  bl_label = "force bpyShaders"
7441  bl_options = {'REGISTER'}
7442 
7443  @classmethod
7444  def poll(cls, context):
7445  if context.active_object and context.active_object.active_material: return True
7446  def invoke(self, context, event):
7447  mat = context.active_object.active_material
7448  mat.use_material_passes = True
7449  bpyShaders.create_material_passes( mat )
7450  return {'FINISHED'}
7451 
7452  ## setup from MaterialScripts.reset_rna( callback=bpyShaders.on_change_parent_material )
7453  @staticmethod
7454  def on_change_parent_material(mat,context):
7455  print(mat,context)
7456  print('callback', mat.ogre_parent_material)
7457 
7458  @staticmethod
7459  def get_subnodes(mat, type='TEXTURE'):
7460  d = {}
7461  for node in mat.nodes:
7462  if node.type==type: d[node.name] = node
7463  keys = list(d.keys())
7464  keys.sort()
7465  r = []
7466  for key in keys: r.append( d[key] )
7467  return r
7468 
7469 
7470  @staticmethod
7471  def get_texture_subnodes( parent, submaterial=None ):
7472  if not submaterial: submaterial = parent.active_node_material
7473  d = {}
7474  for link in parent.node_tree.links:
7475  if link.from_node and link.from_node.type=='TEXTURE':
7476  if link.to_node and link.to_node.type == 'MATERIAL_EXT':
7477  if link.to_node.material:
7478  if link.to_node.material.name == submaterial.name:
7479  node = link.from_node
7480  d[node.name] = node
7481  keys = list(d.keys()) # this breaks if the user renames the node - TODO improve me
7482  keys.sort()
7483  r = []
7484  for key in keys: r.append( d[key] )
7485  return r
7486 
7487  @staticmethod
7488  def get_connected_input_nodes( material, node ):
7489  r = []
7490  for link in material.node_tree.links:
7491  if link.to_node and link.to_node.name == node.name:
7492  r.append( link.from_node )
7493  return r
7494 
7495  @staticmethod
7496  def get_or_create_material_passes( mat, n=8 ):
7497  if not mat.node_tree:
7498  print('CREATING MATERIAL PASSES', n)
7499  bpyShaders.create_material_passes( mat, n )
7500 
7501  d = {} # funky, blender259 had this in order, now blender260 has random order
7502  for node in mat.node_tree.nodes:
7503  if node.type == 'MATERIAL_EXT' and node.name.startswith('GEN.'):
7504  d[node.name] = node
7505  keys = list(d.keys())
7506  keys.sort()
7507  r = []
7508  for key in keys: r.append( d[key] )
7509  return r
7510 
7511  @staticmethod
7512  def get_or_create_texture_nodes( mat, n=6 ): # currently not used
7513  #print('bpyShaders.get_or_create_texture_nodes( %s, %s )' %(mat,n))
7514  assert mat.node_tree # must call create_material_passes first
7515  m = []
7516  for node in mat.node_tree.nodes:
7517  if node.type == 'MATERIAL_EXT' and node.name.startswith('GEN.'):
7518  m.append( node )
7519  if not m:
7520  m = bpyShaders.get_or_create_material_passes(mat)
7521  print(m)
7522  r = []
7523  for link in mat.node_tree.links:
7524  print(link, link.to_node, link.from_node)
7525  if link.to_node and link.to_node.name.startswith('GEN.') and link.from_node.type=='TEXTURE':
7526  r.append( link.from_node )
7527  if not r:
7528  print('--missing texture nodes--')
7529  r = bpyShaders.create_texture_nodes( mat, n )
7530  return r
7531 
7532  @staticmethod
7533  def create_material_passes( mat, n=8, textures=True ):
7534  #print('bpyShaders.create_material_passes( %s, %s )' %(mat,n))
7535  mat.use_nodes = True
7536  tree = mat.node_tree # valid pointer now
7537 
7538  nodes = bpyShaders.get_subnodes( tree, 'MATERIAL' ) # assign base material
7539  if nodes and not nodes[0].material:
7540  nodes[0].material = mat
7541 
7542  r = []
7543  x = 680
7544  for i in range( n ):
7545  node = tree.nodes.new( type='MATERIAL_EXT' )
7546  node.name = 'GEN.%s' %i
7547  node.location.x = x; node.location.y = 640
7548  r.append( node )
7549  x += 220
7550  #mat.use_nodes = False # TODO set user material to default output
7551  if textures:
7552  texnodes = bpyShaders.create_texture_nodes( mat )
7553  print( texnodes )
7554  return r
7555 
7556  @staticmethod
7557  def create_texture_nodes( mat, n=6, geoms=True ):
7558  #print('bpyShaders.create_texture_nodes( %s )' %mat)
7559  assert mat.node_tree # must call create_material_passes first
7560  mats = bpyShaders.get_or_create_material_passes( mat )
7561  r = {}; x = 400
7562  for i,m in enumerate(mats):
7563  r['material'] = m; r['textures'] = []; r['geoms'] = []
7564  inputs = [] # other inputs mess up material preview #
7565  for tag in ['Mirror', 'Ambient', 'Emit', 'SpecTra', 'Ray Mirror', 'Translucency']:
7566  inputs.append( m.inputs[ tag ] )
7567  for j in range(n):
7568  tex = mat.node_tree.nodes.new( type='TEXTURE' )
7569  tex.name = 'TEX.%s.%s' %(j, m.name)
7570  tex.location.x = x - (j*16)
7571  tex.location.y = -(j*230)
7572  input = inputs[j]; output = tex.outputs['Color']
7573  link = mat.node_tree.links.new( input, output )
7574  r['textures'].append( tex )
7575  if geoms:
7576  geo = mat.node_tree.nodes.new( type='GEOMETRY' )
7577  link = mat.node_tree.links.new( tex.inputs['Vector'], geo.outputs['UV'] )
7578  geo.location.x = x - (j*16) - 250
7579  geo.location.y = -(j*250) - 1500
7580  r['geoms'].append( geo )
7581  x += 220
7582  return r
7583 
7584 @UI
7585 class PANEL_node_editor_ui( bpy.types.Panel ):
7586  bl_space_type = 'NODE_EDITOR'
7587  bl_region_type = 'UI'
7588  bl_label = "Ogre Material"
7589 
7590  @classmethod
7591  def poll(self,context):
7592  if context.space_data.id:
7593  return True
7594 
7595  def draw(self, context):
7596  layout = self.layout
7597  topmat = context.space_data.id # the top level node_tree
7598  mat = topmat.active_node_material # the currently selected sub-material
7599  if not mat or topmat.name == mat.name:
7600  self.bl_label = topmat.name
7601  if not topmat.use_material_passes:
7602  layout.operator(
7603  'ogre.force_setup_material_passes',
7604  text="Ogre Material Layers",
7605  icon='SCENE_DATA'
7606  )
7607  ogre_material_panel( layout, topmat, show_programs=False )
7608  elif mat:
7609  self.bl_label = mat.name
7610  ogre_material_panel( layout, mat, topmat, show_programs=False )
7611 
7612 @UI
7613 class PANEL_node_editor_ui_extra( bpy.types.Panel ):
7614  bl_space_type = 'NODE_EDITOR'
7615  bl_region_type = 'UI'
7616  bl_label = "Ogre Material Advanced"
7617  bl_options = {'DEFAULT_CLOSED'}
7618  @classmethod
7619  def poll(self,context):
7620  if context.space_data.id: return True
7621  def draw(self, context):
7622  layout = self.layout
7623  topmat = context.space_data.id # the top level node_tree
7624  mat = topmat.active_node_material # the currently selected sub-material
7625  if mat:
7626  self.bl_label = mat.name + ' (advanced)'
7627  ogre_material_panel_extra( layout, mat )
7628  else:
7629  self.bl_label = topmat.name + ' (advanced)'
7630  ogre_material_panel_extra( layout, topmat )
7631 
7632 def ogre_material_panel_extra( parent, mat ):
7633  box = parent.box()
7634  header = box.row()
7635 
7636  if mat.use_fixed_pipeline:
7637  header.prop( mat, 'use_fixed_pipeline', text='Fixed Pipeline', icon='LAMP_SUN' )
7638  row = box.row()
7639  row.prop(mat, "use_vertex_color_paint", text="Vertex Colors")
7640  row.prop(mat, "use_shadeless")
7641  if mat.use_shadeless and not mat.use_vertex_color_paint:
7642  row = box.row()
7643  row.prop(mat, "diffuse_color", text='')
7644  elif not mat.use_shadeless:
7645  if not mat.use_vertex_color_paint:
7646  row = box.row()
7647  row.prop(mat, "diffuse_color", text='')
7648  row.prop(mat, "diffuse_intensity", text='intensity')
7649  row = box.row()
7650  row.prop(mat, "specular_color", text='')
7651  row.prop(mat, "specular_intensity", text='intensity')
7652  row = box.row()
7653  row.prop(mat, "specular_hardness")
7654  row = box.row()
7655  row.prop(mat, "ambient")
7656  #row = box.row()
7657  row.prop(mat, "emit")
7658  box.prop(mat, 'use_ogre_advanced_options', text='---guru options---' )
7659  else:
7660  header.prop( mat, 'use_fixed_pipeline', text='', icon='LAMP_SUN' )
7661  header.prop(mat, 'use_ogre_advanced_options', text='---guru options---' )
7662 
7663  if mat.use_ogre_advanced_options:
7664  box.prop(mat, 'offset_z')
7665  box.prop(mat, "use_shadows")
7666  box.prop(mat, 'ogre_depth_write' )
7667  for tag in 'ogre_colour_write ogre_lighting ogre_normalise_normals ogre_light_clip_planes ogre_light_scissor ogre_alpha_to_coverage ogre_depth_check'.split():
7668  box.prop(mat, tag)
7669  for tag in 'ogre_polygon_mode ogre_shading ogre_cull_hardware ogre_transparent_sorting ogre_illumination_stage ogre_depth_func ogre_scene_blend_op'.split():
7670  box.prop(mat, tag)
7671 
7672 def ogre_material_panel( layout, mat, parent=None, show_programs=True ):
7673  box = layout.box()
7674  header = box.row()
7675  header.prop(mat, 'ogre_scene_blend', text='')
7676  if mat.ogre_scene_blend and 'alpha' in mat.ogre_scene_blend:
7677  row = box.row()
7678  if mat.use_transparency:
7679  row.prop(mat, "use_transparency", text='')
7680  row.prop(mat, "alpha")
7681  else:
7682  row.prop(mat, "use_transparency", text='Transparent')
7683  if not parent:
7684  return # only allow on pass1 and higher
7685 
7686  header.prop(mat, 'use_ogre_parent_material', icon='FILE_SCRIPT', text='')
7687 
7688  if mat.use_ogre_parent_material:
7689  row = box.row()
7690  row.prop(mat, 'ogre_parent_material', text='')
7691 
7692  s = get_ogre_user_material( mat.ogre_parent_material ) # gets by name
7693  if s and (s.vertex_programs or s.fragment_programs):
7694  progs = s.get_programs()
7695  split = box.row()
7696  texnodes = None
7697 
7698  if parent:
7699  texnodes = bpyShaders.get_texture_subnodes( parent, submaterial=mat )
7700  elif mat.node_tree:
7701  texnodes = bpyShaders.get_texture_subnodes( mat ) # assume toplevel
7702 
7703  if not progs:
7704  bx = split.box()
7705  bx.label( text='(missing shader programs)', icon='ERROR' )
7706  elif s.texture_units and texnodes:
7707  bx = split.box()
7708  for i,name in enumerate(s.texture_units_order):
7709  if i<len(texnodes):
7710  row = bx.row()
7711  #row.label( text=name )
7712  tex = texnodes[i]
7713  row.prop( tex, 'texture', text=name )
7714  if parent:
7715  inputs = bpyShaders.get_connected_input_nodes( parent, tex )
7716  if inputs:
7717  geo = inputs[0]
7718  assert geo.type == 'GEOMETRY'
7719  row.prop( geo, 'uv_layer', text='UV' )
7720  else:
7721  print('WARNING: no slot for texture unit:', name)
7722 
7723  if show_programs and (s.vertex_programs or s.fragment_programs):
7724  bx = box.box()
7725  for name in s.vertex_programs:
7726  bx.label( text=name )
7727  for name in s.fragment_programs:
7728  bx.label( text=name )
7729 
7730 ## Blender addon main entry point.
7731 ## Allows directly running by "blender --python blender2ogre.py"
7732 
7733 if __name__ == "__main__":
7734  register()
def dotmesh(path, facesAddr, facesSmoothAddr, facesMatAddr, vertsPosAddr, vertsNorAddr, numFaces, numVerts, materialNames)


naoqi_tools
Author(s): Mikael Arguedas
autogenerated on Thu Jul 16 2020 03:18:37