Skip to content

Commit

Permalink
feat(shape): Added curve/spline export support
Browse files Browse the repository at this point in the history
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
  • Loading branch information
NMC-TBone committed Nov 23, 2023
1 parent 5924672 commit fbec170
Show file tree
Hide file tree
Showing 4 changed files with 163 additions and 9 deletions.
2 changes: 2 additions & 0 deletions addon/i3dio/exporter.py
Expand Up @@ -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")

Expand Down
18 changes: 17 additions & 1 deletion addon/i3dio/i3d.py
Expand Up @@ -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] = {}
Expand Down Expand Up @@ -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]

Expand Down
149 changes: 142 additions & 7 deletions addon/i3dio/node_classes/shape.py
Expand Up @@ -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)
Expand Down Expand Up @@ -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()
3 changes: 2 additions & 1 deletion addon/i3dio/ui/exporter.py
Expand Up @@ -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(
Expand Down

0 comments on commit fbec170

Please sign in to comment.