simple_nodes.py 8.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235
  1. """
  2. Any exporters that can be written in a single function can go in here.
  3. Anything more complex should go in it's own file
  4. """
  5. import logging
  6. import math
  7. import mathutils
  8. from ..structures import (
  9. NodeTemplate, fix_directional_transform, gamma_correct, InternalResource,
  10. Map, Array
  11. )
  12. from .animation import export_animation_data, AttributeConvertInfo
  13. from .mesh import export_mesh_node
  14. def export_empty_node(escn_file, export_settings, node, parent_gd_node):
  15. """Converts an empty (or any unknown node) into a spatial"""
  16. if "EMPTY" not in export_settings['object_types']:
  17. return parent_gd_node
  18. empty_node = NodeTemplate(node.name, "Spatial", parent_gd_node)
  19. empty_node['transform'] = node.matrix_local
  20. if node.name.endswith('-colonly') or node.name.endswith('-convcolonly'):
  21. # For empties marked as collision shapes (name ending in -colonly/-convcolonly) Godot uses the draw/display type to pick a
  22. # collision shape
  23. empty_node['__meta__'] = '{{ "empty_draw_type": "{}" }}'.format(node.empty_display_type)
  24. escn_file.add_node(empty_node)
  25. return empty_node
  26. class CameraNode(NodeTemplate):
  27. """Camera node in godot scene"""
  28. _cam_attr_conv = [
  29. # blender attr, godot attr, converter lambda, type
  30. AttributeConvertInfo('clip_end', 'far', lambda x: x),
  31. AttributeConvertInfo('clip_start', 'near', lambda x: x),
  32. AttributeConvertInfo('ortho_scale', 'size', lambda x: x),
  33. ]
  34. def __init__(self, name, parent):
  35. super().__init__(name, "Camera", parent)
  36. @property
  37. def attribute_conversion(self):
  38. """Get a list of quaternary tuple
  39. (blender_attr, godot_attr, lambda converter, attr type)"""
  40. return self._cam_attr_conv
  41. def export_camera_node(escn_file, export_settings, node, parent_gd_node):
  42. """Exports a camera"""
  43. cam_node = CameraNode(node.name, parent_gd_node)
  44. camera = node.data
  45. for item in cam_node.attribute_conversion:
  46. blender_attr, gd_attr, converter = item
  47. cam_node[gd_attr] = converter(getattr(camera, blender_attr))
  48. if camera.type == "PERSP":
  49. cam_node['projection'] = 0
  50. else:
  51. cam_node['projection'] = 1
  52. # `fov` does not go into `attribute_conversion`, because it can not
  53. # be animated
  54. cam_node['fov'] = math.degrees(camera.angle)
  55. cam_node['transform'] = fix_directional_transform(node.matrix_local)
  56. escn_file.add_node(cam_node)
  57. export_animation_data(escn_file, export_settings,
  58. cam_node, node.data, 'camera')
  59. return cam_node
  60. def find_shader_node(node_tree, name):
  61. """Find the shader node from the tree with the given name."""
  62. for node in node_tree.nodes:
  63. if node.bl_idname == name:
  64. return node
  65. logging.warning("%s node not found", name)
  66. return None
  67. def node_input(node, name):
  68. """Get the named input value from the shader node."""
  69. for inp in node.inputs:
  70. if inp.name == name:
  71. return inp.default_value
  72. logging.warning("%s input not found in %s", name, node.bl_idname)
  73. return None
  74. class LightNode(NodeTemplate):
  75. """Base class for godot light node"""
  76. _light_attr_conv = [
  77. AttributeConvertInfo(
  78. 'specular_factor', 'light_specular', lambda x: x),
  79. AttributeConvertInfo('color', 'light_color', gamma_correct),
  80. AttributeConvertInfo('shadow_color', 'shadow_color', gamma_correct),
  81. ]
  82. _omni_attr_conv = [
  83. AttributeConvertInfo(
  84. 'energy', 'light_energy', lambda x: abs(x / 100.0)),
  85. AttributeConvertInfo('cutoff_distance', 'omni_range', lambda x: x),
  86. ]
  87. _spot_attr_conv = [
  88. AttributeConvertInfo(
  89. 'energy', 'light_energy', lambda x: abs(x / 100.0)),
  90. AttributeConvertInfo(
  91. 'spot_size', 'spot_angle', lambda x: math.degrees(x/2)
  92. ),
  93. AttributeConvertInfo(
  94. 'spot_blend', 'spot_angle_attenuation', lambda x: 0.2/(x + 0.01)
  95. ),
  96. AttributeConvertInfo('cutoff_distance', 'spot_range', lambda x: x),
  97. ]
  98. _directional_attr_conv = [
  99. AttributeConvertInfo('energy', 'light_energy', abs),
  100. ]
  101. @property
  102. def attribute_conversion(self):
  103. """Get a list of quaternary tuple
  104. (blender_attr, godot_attr, lambda converter, attr type)"""
  105. if self.get_type() == 'OmniLight':
  106. return self._light_attr_conv + self._omni_attr_conv
  107. if self.get_type() == 'SpotLight':
  108. return self._light_attr_conv + self._spot_attr_conv
  109. if self.get_type() == 'DirectionalLight':
  110. return self._light_attr_conv + self._directional_attr_conv
  111. return self._light_attr_conv
  112. def export_light_node(escn_file, export_settings, node, parent_gd_node):
  113. """Exports lights - well, the ones it knows about. Other light types
  114. just throw a warning"""
  115. bl_light_to_gd_light = {
  116. "POINT": "OmniLight",
  117. "SPOT": "SpotLight",
  118. "SUN": "DirectionalLight",
  119. }
  120. light = node.data
  121. if light.type in bl_light_to_gd_light:
  122. light_node = LightNode(
  123. node.name, bl_light_to_gd_light[light.type], parent_gd_node)
  124. else:
  125. light_node = None
  126. logging.warning(
  127. "%s light is not supported. Use Point, Spot or Sun", node.name
  128. )
  129. if light_node is not None:
  130. for item in light_node.attribute_conversion:
  131. bl_attr, gd_attr, converter = item
  132. light_node[gd_attr] = converter(getattr(light, bl_attr))
  133. # Properties common to all lights
  134. # These cannot be set via AttributeConvertInfo as it will not handle
  135. # animations correctly
  136. light_node['transform'] = fix_directional_transform(node.matrix_local)
  137. light_node['light_negative'] = light.energy < 0
  138. light_node['shadow_enabled'] = (
  139. light.use_shadow and light.cycles.cast_shadow)
  140. escn_file.add_node(light_node)
  141. export_animation_data(escn_file, export_settings,
  142. light_node, node.data, 'light')
  143. return light_node
  144. def _export_spline(escn_file, spline, name):
  145. points = Array("PoolVector3Array(")
  146. tilts = Array("PoolRealArray(")
  147. axis_correct = mathutils.Matrix((
  148. (1, 0, 0), # X in blender is X in Godot
  149. (0, 0, -1), # Y in blender is -Z in Godot
  150. (0, 1, 0), # Z in blender is Y in Godot
  151. )).normalized()
  152. src_points = spline.bezier_points
  153. if spline.use_cyclic_u:
  154. # Godot fakes a closed path by adding the start point at the end
  155. # https://github.com/godotengine/godot-proposals/issues/527
  156. src_points = [*src_points, src_points[0]]
  157. for point in src_points:
  158. # blender handles are absolute
  159. # godot handles are relative to the control point
  160. points.extend((point.handle_left - point.co) @ axis_correct)
  161. points.extend((point.handle_right - point.co) @ axis_correct)
  162. points.extend(point.co @ axis_correct)
  163. tilts.append(point.tilt)
  164. data = Map()
  165. data["points"] = points
  166. data["tilts"] = tilts
  167. curve_resource = InternalResource("Curve3D", name)
  168. curve_resource["_data"] = data
  169. curve_id = escn_file.get_internal_resource(spline)
  170. if curve_id is None:
  171. return escn_file.add_internal_resource(curve_resource, spline)
  172. return escn_file.get_internal_resource(spline)
  173. def export_curve_node(escn_file, export_settings, node, parent_gd_node):
  174. """Export a curve to a Path node, with a child mesh."""
  175. splines = node.data.splines
  176. path_node = NodeTemplate(node.name, "Path", parent_gd_node)
  177. path_node["transform"] = node.matrix_local
  178. escn_file.add_node(path_node)
  179. for spline in splines:
  180. # https://docs.blender.org/manual/en/latest/modeling/curves/editing/curve.html#set-spline-type
  181. # Godot only supports bezier, and blender cannot convert losslessly
  182. if spline.type == "BEZIER":
  183. curve_id = _export_spline(escn_file, splines[0], node.data.name)
  184. if spline == splines.active:
  185. path_node["curve"] = "SubResource({})".format(curve_id)
  186. # Create child MeshInstance renders the bevel for any curve type
  187. mesh_node = export_mesh_node(
  188. escn_file, export_settings, node, path_node
  189. )
  190. # The transform is already set on the path, don't duplicate it
  191. mesh_node["transform"] = mathutils.Matrix.Identity(4)
  192. return path_node