PageRenderTime 119ms CodeModel.GetById 25ms RepoModel.GetById 0ms app.codeStats 1ms

/io_export_ogreDotScene.py

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