From fbec1706c26ea7c5f5e5f15cc596e8bb407b203e Mon Sep 17 00:00:00 2001 From: Kristian <57712777+NMC-TBone@users.noreply.github.com> Date: Thu, 23 Nov 2023 23:37:49 +0100 Subject: [PATCH] feat(shape): Added curve/spline export support feat(shape): Added curve/spline export support - Supports all curve spline types (BEZIER, NURBS, POLY) - POLY = linear curve - Curve shape will most likely differ between what is seen in Blender and what appears in GE. The exported control points are the same, but Blender and GE does not use the save type of curve and no effort has been made to convert between them at this point - This curve export is equally to the one done by the official GE addon --- addon/i3dio/exporter.py | 2 + addon/i3dio/i3d.py | 18 +++- addon/i3dio/node_classes/shape.py | 149 ++++++++++++++++++++++++++++-- addon/i3dio/ui/exporter.py | 3 +- 4 files changed, 163 insertions(+), 9 deletions(-) diff --git a/addon/i3dio/exporter.py b/addon/i3dio/exporter.py index 76904b6..f3cc95a 100644 --- a/addon/i3dio/exporter.py +++ b/addon/i3dio/exporter.py @@ -228,6 +228,8 @@ def _add_object_to_i3d(i3d: I3D, obj: BlenderObject, parent: SceneGraphNode = No node = i3d.add_light_node(obj, _parent) elif obj.type == 'CAMERA': node = i3d.add_camera_node(obj, _parent) + elif obj.type == 'CURVE': + node = i3d.add_shape_node(obj, _parent) else: raise NotImplementedError(f"Object type: {obj.type!r} is not supported yet") diff --git a/addon/i3dio/i3d.py b/addon/i3dio/i3d.py index 90b8cda..c545d70 100644 --- a/addon/i3dio/i3d.py +++ b/addon/i3dio/i3d.py @@ -38,7 +38,7 @@ def __init__(self, name: str, i3d_file_path: str, conversion_matrix: mathutils.M self.scene_root_nodes = [] self.conversion_matrix = conversion_matrix - self.shapes: Dict[Union[str, int], IndexedTriangleSet] = {} + self.shapes: Dict[Union[str, int], Union[IndexedTriangleSet, NurbsCurve]] = {} self.materials: Dict[Union[str, int], Material] = {} self.files: Dict[Union[str, int], File] = {} self.merge_groups: Dict[int, MergeGroup] = {} @@ -161,6 +161,22 @@ def add_shape(self, evaluated_mesh: EvaluatedMesh, shape_name: Optional[str] = N return shape_id return self.shapes[name].id + def add_curve(self, evaluated_curve: EvaluatedNurbsCurve, curve_name: Optional[str] = None) -> int: + if curve_name is None: + name = evaluated_curve.name + else: + name = curve_name + + if name not in self.shapes: + curve_id = self._next_available_id('shape') + nurbs_curve = NurbsCurve(curve_id, self, evaluated_curve, curve_name) + # Store a reference to the curve from both its name and its curve id + self.shapes.update(dict.fromkeys([curve_id, name], nurbs_curve)) + self.xml_elements['Shapes'].append(nurbs_curve.element) + return curve_id + return self.shapes[name].id + + def get_shape_by_id(self, shape_id: int): return self.shapes[shape_id] diff --git a/addon/i3dio/node_classes/shape.py b/addon/i3dio/node_classes/shape.py index f7a5008..6a641b5 100644 --- a/addon/i3dio/node_classes/shape.py +++ b/addon/i3dio/node_classes/shape.py @@ -2,7 +2,7 @@ import mathutils import collections import logging -from typing import (OrderedDict, Optional, List, Dict, ChainMap) +from typing import (OrderedDict, Optional, List, Dict, ChainMap, Union) import bpy from .node import (Node, SceneGraphNode) @@ -431,25 +431,160 @@ def populate_xml_element(self): self.material_indexes = self.material_indexes.strip() +class ControlVertex: + def __init__(self, position): + self._position = position + self._str = '' + self._make_hash_string() + + def _make_hash_string(self): + self._str = f"{self._position}" + + def __str__(self): + return self._str + + def __hash__(self): + return hash(self._str) + + def __eq__(self, other): + return f"{self!s}" == f'{other!s}' + + def position_for_xml(self): + return "{0:.6f} {1:.6f} {2:.6f}".format(*self._position) + + +class EvaluatedNurbsCurve: + def __init__(self, i3d: I3D, shape_object: bpy.types.Object, name: str = None, + reference_frame: mathutils.Matrix = None): + if name is None: + self.name = shape_object.data.name + else: + self.name = name + self.i3d = i3d + self.object = None + self.curve_data = None + self.logger = debugging.ObjectNameAdapter(logging.getLogger(f"{__name__}.{type(self).__name__}"), + {'object_name': self.name}) + self.control_vertices = [] + self.generate_evaluated_curve(shape_object, reference_frame) + + def generate_evaluated_curve(self, shape_object: bpy.types.Object, reference_frame: mathutils.Matrix = None): + self.object = shape_object + + self.curve_data = self.object.to_curve(depsgraph=self.i3d.depsgraph) + + # If a reference is given transform the generated mesh by that frame to place it somewhere else than center of + # the mesh origo + if reference_frame is not None: + self.curve_data.transform(reference_frame.inverted() @ self.object.matrix_world) + + conversion_matrix = self.i3d.conversion_matrix + if self.i3d.get_setting('apply_unit_scale'): + self.logger.debug(f"applying unit scaling") + conversion_matrix = \ + mathutils.Matrix.Scale(bpy.context.scene.unit_settings.scale_length, 4) @ conversion_matrix + + self.curve_data.transform(conversion_matrix) + + +class NurbsCurve(Node): + ELEMENT_TAG = 'NurbsCurve' + NAME_FIELD_NAME = 'name' + ID_FIELD_NAME = 'shapeId' + + def __init__(self, id_: int, i3d: I3D, evaluated_curve_data: EvaluatedNurbsCurve, shape_name: Optional[str] = None): + self.id: int = id_ + self.i3d: I3D = i3d + self.evaluated_curve_data: EvaluatedNurbsCurve = evaluated_curve_data + self.control_vertex: OrderedDict[ControlVertex, int] = collections.OrderedDict() + self.spline_type = None + self.spline_form = None + if shape_name is None: + self.shape_name = self.evaluated_curve_data.name + else: + self.shape_name = shape_name + super().__init__(id_, i3d, None) + + @property + def name(self): + return self.shape_name + + @property + def element(self): + return self.xml_elements['node'] + + @element.setter + def element(self, value): + self.xml_elements['node'] = value + + def process_spline(self, spline): + if spline.type == 'BEZIER': + points = spline.bezier_points + self.spline_type = "cubic" + elif spline.type == 'NURBS': + points = spline.points + self.spline_type = "cubic" + elif spline.type == 'POLY': + points = spline.points + self.spline_type = "linear" + else: + self.logger.warning(f"{spline.type} is not supported! Export of this curve is aborted.") + return + + for loop_index, point in enumerate(points): + ctrl_vertex = ControlVertex(point.co.xyz) + self.control_vertex[ctrl_vertex] = loop_index + + self.spline_form = "closed" if spline.use_cyclic_u else "open" + + def populate_from_evaluated_nurbscurve(self): + spline = self.evaluated_curve_data.curve_data.splines[0] + self.process_spline(spline) + + def write_control_vertices(self): + for control_vertex in list(self.control_vertex.keys()): + vertex_attributes = {'c': control_vertex.position_for_xml()} + + xml_i3d.SubElement(self.element, 'cv', vertex_attributes) + + def populate_xml_element(self): + if len(self.evaluated_curve_data.curve_data.splines) == 0: + self.logger.warning(f"has no splines! Export of this curve is aborted.") + return + + self.populate_from_evaluated_nurbscurve() + if self.spline_type: + self._write_attribute('type', self.spline_type, 'node') + if self.spline_form: + self._write_attribute('form', self.spline_form, 'node') + self.logger.debug(f"Has '{len(self.control_vertex)}' control vertices") + self.write_control_vertices() + + class ShapeNode(SceneGraphNode): ELEMENT_TAG = 'Shape' - def __init__(self, id_: int, mesh_object: [bpy.types.Object, None], i3d: I3D, - parent: [SceneGraphNode or None] = None): + def __init__(self, id_: int, shape_object: Optional[bpy.types.Object], i3d: I3D, + parent: Optional[SceneGraphNode] = None): self.shape_id = None - super().__init__(id_=id_, blender_object=mesh_object, i3d=i3d, parent=parent) + super().__init__(id_=id_, blender_object=shape_object, i3d=i3d, parent=parent) @property def _transform_for_conversion(self) -> mathutils.Matrix: return self.i3d.conversion_matrix @ self.blender_object.matrix_local @ self.i3d.conversion_matrix.inverted() def add_shape(self): - self.shape_id = self.i3d.add_shape(EvaluatedMesh(self.i3d, self.blender_object)) - self.xml_elements['IndexedTriangleSet'] = self.i3d.shapes[self.shape_id].element + if self.blender_object.type == 'CURVE': + self.shape_id = self.i3d.add_curve(EvaluatedNurbsCurve(self.i3d, self.blender_object)) + self.xml_elements['NurbsCurve'] = self.i3d.shapes[self.shape_id].element + else: + self.shape_id = self.i3d.add_shape(EvaluatedMesh(self.i3d, self.blender_object)) + self.xml_elements['IndexedTriangleSet'] = self.i3d.shapes[self.shape_id].element def populate_xml_element(self): self.add_shape() self.logger.debug(f"has shape ID '{self.shape_id}'") self._write_attribute('shapeId', self.shape_id) - self._write_attribute('materialIds', self.i3d.shapes[self.shape_id].material_indexes) + if self.blender_object.type == 'MESH': + self._write_attribute('materialIds', self.i3d.shapes[self.shape_id].material_indexes) super().populate_xml_element() diff --git a/addon/i3dio/ui/exporter.py b/addon/i3dio/ui/exporter.py index ad1f346..2b6469b 100644 --- a/addon/i3dio/ui/exporter.py +++ b/addon/i3dio/ui/exporter.py @@ -86,10 +86,11 @@ class I3DExportUIProperties(bpy.types.PropertyGroup): ('CAMERA', "Camera", "Export cameras"), ('LIGHT', "Light", "Export lights"), ('MESH', "Mesh", "Export meshes"), + ('CURVE', "Curve", "Export curves"), ('ARMATURE', "Armatures", "Export armatures, used for skinned meshes") ), options={'ENUM_FLAG'}, - default={'EMPTY', 'CAMERA', 'LIGHT', 'MESH', 'ARMATURE'}, + default={'EMPTY', 'CAMERA', 'LIGHT', 'MESH', 'CURVE', 'ARMATURE'}, ) features_to_export: EnumProperty(