Skip to content

Commit

Permalink
Merge branch 'develop'
Browse files Browse the repository at this point in the history
  • Loading branch information
StjerneIdioten committed Nov 24, 2023
2 parents 46da68c + 8b260c3 commit feb20a0
Show file tree
Hide file tree
Showing 70 changed files with 345 additions and 112 deletions.
4 changes: 3 additions & 1 deletion addon/i3dio/exporter.py
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@ def export_blend_to_i3d(filepath: str, axis_forward, axis_up) -> dict:
logger.error(f"Empty Game Path")
else:
try:
conversion_result = subprocess.run(args=[str(i3d_binarize_path), '-in', str(filepath), '-out', str(filepath), '-gamePath', str(game_path)], timeout=BINARIZER_TIMEOUT_IN_SECONDS, check=True, text=True, stdout = subprocess.PIPE, stderr=subprocess.STDOUT)
conversion_result = subprocess.run(args=[str(i3d_binarize_path), '-in', str(filepath), '-out', str(filepath), '-gamePath', f"{game_path}/"], timeout=BINARIZER_TIMEOUT_IN_SECONDS, check=True, text=True, stdout = subprocess.PIPE, stderr=subprocess.STDOUT)
except FileNotFoundError as e:
logger.error(f'Invalid path to i3dConverter.exe: "{i3d_binarize_path}"')
except subprocess.TimeoutExpired as e:
Expand Down 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
111 changes: 72 additions & 39 deletions addon/i3dio/i3d.py
Original file line number Diff line number Diff line change
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 Expand Up @@ -218,6 +234,9 @@ def add_file_image(self, path_to_file: str) -> int:
def add_file_shader(self, path_to_file: str) -> int:
return self.add_file(Shader, path_to_file)

def add_file_reference(self, path_to_file: str) -> int:
return self.add_file(Reference, path_to_file)

def get_setting(self, setting: str):
return self.settings[setting]

Expand Down Expand Up @@ -249,44 +268,58 @@ def export_to_i3d_file(self) -> None:
self.export_i3d_mapping()

def export_i3d_mapping(self) -> None:
tree = xml_i3d.parse(bpy.path.abspath(self.settings['i3d_mapping_file_path']))
if tree is None:
self.logger.warning(f"Supplied mapping file is not correct xml, failed with error")
else:
root = tree.getroot()
i3d_mappings_element = root.find('i3dMappings')
if i3d_mappings_element is not None:
if self.settings['i3d_mapping_overwrite_mode'] == 'CLEAN':
i3d_mappings_element.clear()
elif self.settings['i3d_mapping_overwrite_mode'] == 'OVERWRITE':
pass

def build_index_string(node_to_index):
if node_to_index.parent is None:
index = f"{self.scene_root_nodes.index(node_to_index):d}>"
else:
index = build_index_string(node_to_index.parent)
if index[-1] != '>':
index += '|'
index += str(node_to_index.parent.children.index(node_to_index))
return index

for mapping_node in self.i3d_mapping:
if getattr(mapping_node.blender_object.i3d_mapping, 'mapping_name') != '':
name = getattr(mapping_node.blender_object.i3d_mapping, 'mapping_name')
else:
name = mapping_node.name

attributes = {'id': name, 'node': build_index_string(mapping_node)}
xml_i3d.SubElement(i3d_mappings_element, 'i3dMapping', attributes)

xml_i3d.write_tree_to_file(tree, bpy.path.abspath(self.settings['i3d_mapping_file_path']),
xml_declaration=True,
encoding='utf-8')
else:
self.logger.warning(f"Supplied mapping file does not contain an <i3dMappings> tag anywhere! Cannot"
f"export mappings.")

with open(bpy.path.abspath(self.settings['i3d_mapping_file_path']), 'r+') as xml_file:
vehicle_xml = []
i3d_mapping_idx = None
i3d_mapping_end_found = False
for idx,line in enumerate(xml_file):
if i3d_mapping_idx is None:
if '<i3dMappings>' in line:
i3d_mapping_idx = idx
vehicle_xml.append(line)
xml_indentation = line[0:line.find('<')]

if i3d_mapping_idx is None or i3d_mapping_end_found:
vehicle_xml.append(line)

if not (i3d_mapping_idx is None or i3d_mapping_end_found):
i3d_mapping_end_found = True if '</i3dMappings>' in line else False

if i3d_mapping_idx is None:
for i in reversed(range(len(vehicle_xml))):
if vehicle_xml[i].startswith('</vehicle>'):
xml_indentation = ' '*4
vehicle_xml.insert(i, f"\n{xml_indentation}<i3dMappings>\n")
i3d_mapping_idx = i
self.logger.info(f"Vehicle file does not have an <i3dMappings> tag, inserting one above </vehicle> with default indentation")
break

if i3d_mapping_idx is None:
self.logger.warning(f"Cannot export i3d mapping, provided file has no <i3dMappings> or root level <vehicle> tag!")
return

def build_index_string(node_to_index):
if node_to_index.parent is None:
index = f"{self.scene_root_nodes.index(node_to_index):d}>"
else:
index = build_index_string(node_to_index.parent)
if index[-1] != '>':
index += '|'
index += str(node_to_index.parent.children.index(node_to_index))
return index

for mapping_node in self.i3d_mapping:
# If the mapping is an empty string, use the node name
if not (mapping_name := getattr(mapping_node.blender_object.i3d_mapping, 'mapping_name')):
mapping_name = mapping_node.name

vehicle_xml[i3d_mapping_idx] += f'{xml_indentation*2}<i3dMapping id="{mapping_name}" node="{build_index_string(mapping_node)}" />\n'

vehicle_xml[i3d_mapping_idx] += f'{xml_indentation}</i3dMappings>\n'

xml_file.seek(0)
xml_file.truncate()
xml_file.writelines(vehicle_xml)

# To avoid a circular import, since all nodes rely on the I3D class, but i3d itself contains all the different nodes.
from i3dio.node_classes.node import *
Expand Down
6 changes: 5 additions & 1 deletion addon/i3dio/node_classes/file.py
Original file line number Diff line number Diff line change
Expand Up @@ -117,4 +117,8 @@ class Image(File):


class Shader(File):
MODHUB_FOLDER = 'shaders'
MODHUB_FOLDER = 'shaders'


class Reference(File):
MODHUB_FOLDER = 'assets'
8 changes: 8 additions & 0 deletions addon/i3dio/node_classes/node.py
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,13 @@ def _write_user_attributes(self):
except AttributeError:
pass

def _add_reference_file(self):
if self.blender_object.i3d_reference_path == "" or not self.blender_object.i3d_reference_path.endswith('.i3d'):
return
self.logger.debug(f"Adding reference file")
file_id = self.i3d.add_file_reference(self.blender_object.i3d_reference_path)
self._write_attribute('referenceId', file_id)

@property
@abstractmethod
def _transform_for_conversion(self) -> Union[mathutils.Matrix, None]:
Expand Down Expand Up @@ -206,6 +213,7 @@ def populate_xml_element(self):
self._write_properties()
self._write_user_attributes()
self._add_transform_to_xml_element(self._transform_for_conversion)
self._add_reference_file()

def add_child(self, node: SceneGraphNode):
self.children.append(node)
Expand Down
149 changes: 142 additions & 7 deletions addon/i3dio/node_classes/shape.py
Original file line number Diff line number Diff line change
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()
Loading

0 comments on commit feb20a0

Please sign in to comment.