json_exporter.py 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313
  1. from .animation import *
  2. from .armature import *
  3. from .camera import *
  4. from .exporter_settings_panel import *
  5. from .light_shadow import *
  6. from .logger import *
  7. from .material import *
  8. from .mesh import *
  9. from .package_level import *
  10. from .sound import *
  11. from .world import *
  12. import bpy
  13. from io import open
  14. from os import path, makedirs
  15. #===============================================================================
  16. class JsonExporter:
  17. nameSpace = None # assigned in execute
  18. # - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
  19. def execute(self, context, filepath):
  20. scene = context.scene
  21. self.scene = scene # reference for passing
  22. self.fatalError = None
  23. try:
  24. self.filepathMinusExtension = filepath.rpartition('.')[0]
  25. JsonExporter.nameSpace = getNameSpace(self.filepathMinusExtension)
  26. log = Logger(self.filepathMinusExtension + '.log')
  27. if bpy.ops.object.mode_set.poll():
  28. bpy.ops.object.mode_set(mode = 'OBJECT')
  29. # assign texture location, purely temporary if in-lining
  30. self.textureDir = path.dirname(filepath)
  31. if not scene.inlineTextures:
  32. self.textureDir = path.join(self.textureDir, scene.textureDir)
  33. if not path.isdir(self.textureDir):
  34. makedirs(self.textureDir)
  35. Logger.warn('Texture sub-directory did not already exist, created: ' + self.textureDir)
  36. Logger.log('========= Conversion from Blender to Babylon.js =========', 0)
  37. Logger.log('Scene settings used:', 1)
  38. Logger.log('selected layers only: ' + format_bool(scene.export_onlySelectedLayer), 2)
  39. Logger.log('flat shading entire scene: ' + format_bool(scene.export_flatshadeScene), 2)
  40. Logger.log('inline textures: ' + format_bool(scene.inlineTextures), 2)
  41. if not scene.inlineTextures:
  42. Logger.log('texture directory: ' + self.textureDir, 2)
  43. self.world = World(scene)
  44. bpy.ops.screen.animation_cancel()
  45. currentFrame = bpy.context.scene.frame_current
  46. # Active camera
  47. if scene.camera != None:
  48. self.activeCamera = scene.camera.name
  49. else:
  50. Logger.warn('No active camera has been assigned, or is not in a currently selected Blender layer')
  51. self.cameras = []
  52. self.lights = []
  53. self.shadowGenerators = []
  54. self.skeletons = []
  55. skeletonId = 0
  56. self.meshesAndNodes = []
  57. self.morphTargetMngrs = []
  58. self.materials = []
  59. self.multiMaterials = []
  60. self.sounds = []
  61. self.needPhysics = False
  62. # Scene level sound
  63. if scene.attachedSound != '':
  64. self.sounds.append(Sound(scene.attachedSound, scene.autoPlaySound, scene.loopSound))
  65. # separate loop doing all skeletons, so available in Mesh to make skipping IK bones possible
  66. for object in [object for object in scene.objects]:
  67. scene.frame_set(currentFrame)
  68. if object.type == 'ARMATURE': #skeleton.pose.bones
  69. if object.is_visible(scene):
  70. self.skeletons.append(Skeleton(object, scene, skeletonId, scene.ignoreIKBones))
  71. skeletonId += 1
  72. else:
  73. Logger.warn('The following armature not visible in scene thus ignored: ' + object.name)
  74. # exclude lamps in this pass, so ShadowGenerator constructor can be passed meshesAnNodes
  75. for object in [object for object in scene.objects]:
  76. scene.frame_set(currentFrame)
  77. if object.type == 'CAMERA':
  78. if object.is_visible(scene): # no isInSelectedLayer() required, is_visible() handles this for them
  79. self.cameras.append(Camera(object))
  80. else:
  81. Logger.warn('The following camera not visible in scene thus ignored: ' + object.name)
  82. elif object.type == 'MESH':
  83. forcedParent = None
  84. nameID = ''
  85. nextStartFace = 0
  86. while True and self.isInSelectedLayer(object, scene):
  87. mesh = Mesh(object, scene, nextStartFace, forcedParent, nameID, self)
  88. if mesh.hasUnappliedTransforms and hasattr(mesh, 'skeletonWeights'):
  89. self.fatalError = 'Mesh: ' + mesh.name + ' has un-applied transformations. This will never work for a mesh with an armature. Export cancelled'
  90. Logger.log(self.fatalError)
  91. return
  92. if hasattr(mesh, 'physicsImpostor'): self.needPhysics = True
  93. if hasattr(mesh, 'instances'):
  94. self.meshesAndNodes.append(mesh)
  95. if hasattr(mesh, 'morphTargetManagerId'):
  96. self.morphTargetMngrs.append(mesh)
  97. else:
  98. break
  99. if object.data.attachedSound != '':
  100. self.sounds.append(Sound(object.data.attachedSound, object.data.autoPlaySound, object.data.loopSound, object))
  101. nextStartFace = mesh.offsetFace
  102. if nextStartFace == 0:
  103. break
  104. if forcedParent is None:
  105. nameID = 0
  106. forcedParent = object
  107. Logger.warn('The following mesh has exceeded the maximum # of vertex elements & will be broken into multiple Babylon meshes: ' + object.name)
  108. nameID = nameID + 1
  109. elif object.type == 'EMPTY':
  110. self.meshesAndNodes.append(Node(object))
  111. elif object.type != 'LAMP' and object.type != 'ARMATURE':
  112. Logger.warn('The following object (type - ' + object.type + ') is not currently exportable thus ignored: ' + object.name)
  113. # Lamp / shadow Generator pass; meshesAnNodes complete & forceParents included
  114. for object in [object for object in scene.objects]:
  115. if object.type == 'LAMP':
  116. if object.is_visible(scene): # no isInSelectedLayer() required, is_visible() handles this for them
  117. bulb = Light(object, self.meshesAndNodes)
  118. self.lights.append(bulb)
  119. if object.data.shadowMap != 'NONE':
  120. if bulb.light_type == DIRECTIONAL_LIGHT or bulb.light_type == SPOT_LIGHT:
  121. self.shadowGenerators.append(ShadowGenerator(object, self.meshesAndNodes, scene))
  122. else:
  123. Logger.warn('Only directional (sun) and spot types of lamp are valid for shadows thus ignored: ' + object.name)
  124. else:
  125. Logger.warn('The following lamp not visible in scene thus ignored: ' + object.name)
  126. bpy.context.scene.frame_set(currentFrame)
  127. # output file
  128. self.to_scene_file()
  129. except:# catch *all* exceptions
  130. log.log_error_stack()
  131. raise
  132. finally:
  133. log.close()
  134. self.nWarnings = log.nWarnings
  135. # - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
  136. def to_scene_file(self):
  137. Logger.log('========= Writing of scene file started =========', 0)
  138. # Open file
  139. file_handler = open(self.filepathMinusExtension + '.babylon', 'w', encoding='utf8')
  140. file_handler.write('{')
  141. file_handler.write('"producer":{"name":"Blender","version":"' + bpy.app.version_string + '","exporter_version":"' + format_exporter_version() + '","file":"' + JsonExporter.nameSpace + '.babylon"},\n')
  142. self.world.to_scene_file(file_handler, self.needPhysics)
  143. # Materials
  144. file_handler.write(',\n"materials":[')
  145. first = True
  146. for material in self.materials:
  147. if first != True:
  148. file_handler.write(',\n')
  149. first = False
  150. material.to_scene_file(file_handler)
  151. file_handler.write(']')
  152. # Multi-materials
  153. file_handler.write(',\n"multiMaterials":[')
  154. first = True
  155. for multimaterial in self.multiMaterials:
  156. if first != True:
  157. file_handler.write(',')
  158. first = False
  159. multimaterial.to_scene_file(file_handler)
  160. file_handler.write(']')
  161. # Armatures/Bones
  162. file_handler.write(',\n"skeletons":[')
  163. first = True
  164. for skeleton in self.skeletons:
  165. if first != True:
  166. file_handler.write(',')
  167. first = False
  168. skeleton.to_scene_file(file_handler)
  169. file_handler.write(']')
  170. # Meshes
  171. file_handler.write(',\n"meshes":[')
  172. first = True
  173. for mesh in self.meshesAndNodes:
  174. if first != True:
  175. file_handler.write(',')
  176. first = False
  177. mesh.to_scene_file(file_handler)
  178. file_handler.write(']')
  179. # Morph targets
  180. file_handler.write(',\n"morphTargetManagers":[')
  181. first = True
  182. for mesh in self.morphTargetMngrs:
  183. if first != True:
  184. file_handler.write(',')
  185. first = False
  186. mesh.write_morphing_file(file_handler)
  187. file_handler.write(']')
  188. # Cameras
  189. file_handler.write(',\n"cameras":[')
  190. first = True
  191. for camera in self.cameras:
  192. if hasattr(camera, 'fatalProblem'): continue
  193. if first != True:
  194. file_handler.write(',')
  195. first = False
  196. camera.update_for_target_attributes(self.meshesAndNodes)
  197. camera.to_scene_file(file_handler)
  198. file_handler.write(']')
  199. # Active camera
  200. if hasattr(self, 'activeCamera'):
  201. write_string(file_handler, 'activeCamera', self.activeCamera)
  202. # Lights
  203. file_handler.write(',\n"lights":[')
  204. first = True
  205. for light in self.lights:
  206. if first != True:
  207. file_handler.write(',')
  208. first = False
  209. light.to_scene_file(file_handler)
  210. file_handler.write(']')
  211. # Shadow generators
  212. file_handler.write(',\n"shadowGenerators":[')
  213. first = True
  214. for shadowGen in self.shadowGenerators:
  215. if first != True:
  216. file_handler.write(',')
  217. first = False
  218. shadowGen.to_scene_file(file_handler)
  219. file_handler.write(']')
  220. # Sounds
  221. if len(self.sounds) > 0:
  222. file_handler.write('\n,"sounds":[')
  223. first = True
  224. for sound in self.sounds:
  225. if first != True:
  226. file_handler.write(',')
  227. first = False
  228. sound.to_scene_file(file_handler)
  229. file_handler.write(']')
  230. # Closing
  231. file_handler.write('\n}')
  232. file_handler.close()
  233. Logger.log('========= Writing of scene file completed =========', 0)
  234. # - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
  235. def getMaterial(self, baseMaterialId):
  236. fullName = JsonExporter.nameSpace + '.' + baseMaterialId
  237. for material in self.materials:
  238. if material.name == fullName:
  239. return material
  240. return None
  241. # - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
  242. def getSourceMeshInstance(self, dataName):
  243. for mesh in self.meshesAndNodes:
  244. # nodes have no 'dataName', cannot be instanced in any case
  245. if hasattr(mesh, 'dataName') and mesh.dataName == dataName:
  246. return mesh
  247. return None
  248. # - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
  249. def isInSelectedLayer(self, obj, scene):
  250. if not scene.export_onlySelectedLayer:
  251. return True
  252. for l in range(0, len(scene.layers)):
  253. if obj.layers[l] and scene.layers[l]:
  254. return True
  255. return False
  256. # - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
  257. def get_skeleton(self, name):
  258. for skeleton in self.skeletons:
  259. if skeleton.name == name:
  260. return skeleton
  261. #really cannot happen, will cause exception in caller
  262. return None