diff --git a/README.md b/README.md index 9d23b7e..d94e6cc 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ # IO DIF Blender plugin to import and export MBG Torque DIF interiors and Torque Constructor CSX files. -Supported Blender Versions: 2.8.0 to 4.3 +Supported Blender Versions: 2.8.0 to 4.5 ## Note @@ -75,7 +75,11 @@ Located in the object properties panel - Marker Path: a curve object that describes the path of the moving platform. Each point will become a Marker - Marker Type: the smoothing to use on each marker - Total Time: the amount of time to complete the path - - Starting Time: the time that the platform should begin + - Start Time: the time that the platform should begin + - Constant Speed: if the marker durations should instead be calculated to maintain a consistent speed + - Speed: max speed in units per second + - Start Index: Calculates Start Time based on marker index + - Pause Duration: The time that the platform should spend at zero-length segments - Game Entity: represents an entity in the dif such as items - Game Class: the class of the entity such as "Item", "StaticShape", etc - Datablock: the datablock of the item. @@ -83,10 +87,12 @@ Located in the object properties panel - Path Trigger: represents a trigger that will be added to the MustChange group - Datablock: the trigger datablock, MBG's types are TriggerGotoTarget and TriggerGotoDelayTarget - Pathed Interior: the target object + - Calculate Target Time: if targetTime property should be created from a target marker index + - Target Index: the marker to target ## Limitations -- No Game Entity rotation support: there isnt even a rotation field for Game Entities in difs, and torque doesnt even use the rotation field explicitly passed as a property +- Limited Game Entity rotation support: rotation field is not properly applied to Game Entities in vanilla Torque ## Previews diff --git a/blender_plugin/io_dif/__init__.py b/blender_plugin/io_dif/__init__.py index 10a8674..e33d6ff 100644 --- a/blender_plugin/io_dif/__init__.py +++ b/blender_plugin/io_dif/__init__.py @@ -45,7 +45,7 @@ "author": "RandomityGuy", "description": "Dif import and export plugin for blender", "blender": (2, 80, 0), - "version": (1, 3, 2), + "version": (1, 3, 3), "location": "File > Import-Export", "warning": "", "category": "Import-Export", @@ -88,6 +88,7 @@ def set_marker_path(self, context): spline.type = 'POLY' class InteriorSettings(bpy.types.PropertyGroup): + # Interiors interior_type: EnumProperty( name="Interior Entity Type", items=( @@ -99,15 +100,15 @@ class InteriorSettings(bpy.types.PropertyGroup): default="static_interior", description="How this object should be interpreted for the exporter.", ) - marker_path: PointerProperty(type=bpy.types.Curve, name="Marker Path", description="The path to create markers from.", update=set_marker_path) - pathed_interior_target: PointerProperty(type=bpy.types.Object, name="Pathed Interior Target", description="The platform to trigger.") - game_entity_datablock: StringProperty(name="Datablock") - game_entity_gameclass: StringProperty(name="Game Class") - game_entity_properties: CollectionProperty( - type=InteriorKVP, name="Custom Properties" - ) - + constant_speed: BoolProperty(name = "Constant Speed", description = "If the marker durations should be based on speed instead of total time.", default=True) + speed: FloatProperty(name="Speed", description="The speed that the platform should be moving at. If using Accelerate smoothing, this is max speed.", default=1, min=0.01, max=100) + total_time: IntProperty(name="Total Time", description="The total time (in ms) from path start to end. Equally divided across each marker on export.", default=3000, min=1) + start_time: IntProperty(name="Start Time", description="The time in the path (in ms) that the platform should be at level restart.", default=0, min=0) + start_index: IntProperty(name="Start Index", description="The marker that the platform should be at level restart (0 is 1st marker).", default=0, min=0, soft_max=10) + pause_duration: IntProperty(name = "Pause Duration", description="At a path segment of length 0, the platform will wait this long (in ms).", default=0, min=0, soft_max=10000) + reverse: BoolProperty(name = "Reverse", description = "If the platform should loop backwards (if not using a trigger).") + marker_type: EnumProperty( name="Marker Type", items=( @@ -118,9 +119,18 @@ class InteriorSettings(bpy.types.PropertyGroup): description="The type of smoothing that should be applied to all markers exported from the path.", ) - total_time: IntProperty(name="Total Time", description="The total time (in ms) from path start to end. Equally divided across each marker on export.", default=3000) - start_time: IntProperty(name="Starting Time", description="The time in the path (in ms) that the platform should be at level restart.", default=0) - reverse: BoolProperty(name = "Reverse", description = "If the platform should loop backwards (if not using a trigger).") + # Triggers + pathed_interior_target: PointerProperty(type=bpy.types.Object, name="Pathed Interior Target", description="The platform to trigger.") + target_marker: BoolProperty(name = "Calculate Target Time", description="If enabled, the targetTime will be calculated to be at a specific marker.", default=True) + target_index: IntProperty(name = "Target Index", description="The marker to target (0 is 1st marker).", default=0, min=0, soft_max=10) + + # Entities + game_entity_datablock: StringProperty(name="Datablock") + game_entity_gameclass: StringProperty(name="Game Class") + game_entity_properties: CollectionProperty( + type=InteriorKVP, name="Custom Properties" + ) + class InteriorPanel(bpy.types.Panel): bl_label = "DIF properties" @@ -131,7 +141,6 @@ class InteriorPanel(bpy.types.Panel): def draw(self, context): layout = self.layout - obj = context sublayout = layout.row() sublayout.prop(context.object.dif_props, "interior_type") #TODO only show this on relevant objects? @@ -145,17 +154,35 @@ def draw(self, context): sublayout = layout.row() sublayout.prop(context.object.dif_props, "marker_type") sublayout = layout.row() - sublayout.prop(context.object.dif_props, "total_time") - sublayout = layout.row() - sublayout.prop(context.object.dif_props, "start_time") + sublayout.prop(context.object.dif_props, "constant_speed") sublayout = layout.row() + if context.object.dif_props.constant_speed: + sublayout.prop(context.object.dif_props, "speed") + sublayout = layout.row() + sublayout.prop(context.object.dif_props, "start_index") + sublayout = layout.row() + sublayout.prop(context.object.dif_props, "pause_duration") + sublayout = layout.row() + else: + sublayout.prop(context.object.dif_props, "total_time") + sublayout = layout.row() + sublayout.prop(context.object.dif_props, "start_time") + sublayout = layout.row() + sublayout.prop(context.object.dif_props, "reverse") + if context.object.dif_props.interior_type in ["game_entity", "path_trigger"]: sublayout = layout.row() sublayout.prop(context.object.dif_props, "game_entity_datablock") sublayout = layout.row() if context.object.dif_props.interior_type == "path_trigger": sublayout.prop(context.object.dif_props, "pathed_interior_target") + sublayout = layout.row() + sublayout.prop(context.object.dif_props, "target_marker") + sublayout = layout.row() + if context.object.dif_props.target_marker: + sublayout.prop(context.object.dif_props, "target_index") + sublayout = layout.row() else: sublayout.prop(context.object.dif_props, "game_entity_gameclass") sublayout = layout.row() @@ -204,7 +231,7 @@ def execute(self, context): ) ) - if bpy.data.is_saved and context.user_preferences.filepaths.use_relative_paths: + if bpy.data.is_saved and context.preferences.filepaths.use_relative_paths: import os keywords["relpath"] = os.path.dirname(bpy.data.filepath) @@ -242,7 +269,7 @@ def execute(self, context): ) ) - if bpy.data.is_saved and context.user_preferences.filepaths.use_relative_paths: + if bpy.data.is_saved and context.preferences.filepaths.use_relative_paths: import os keywords["relpath"] = os.path.dirname(bpy.data.filepath) diff --git a/blender_plugin/io_dif/export_dif.py b/blender_plugin/io_dif/export_dif.py index ca97b57..04c7533 100644 --- a/blender_plugin/io_dif/export_dif.py +++ b/blender_plugin/io_dif/export_dif.py @@ -10,7 +10,7 @@ from bpy.types import Curve, Image, Material, Mesh, Object, ShaderNodeTexImage from bpy_extras.wm_utils.progress_report import ProgressReport, ProgressReportSubstep -from mathutils import Quaternion, Vector +from mathutils import Quaternion, Vector, Matrix if platform.system() == "Windows": dllpath = os.path.join(os.path.dirname(os.path.realpath(__file__)), "DifBuilderLib.dll") @@ -105,10 +105,6 @@ def update_status(stop, current, total, status, finish_status): update_status_c = STATUSFN(update_status) -scene = bpy.context.scene - -obj = bpy.context.active_object - class MarkerList: def __init__(self): self.__ptr__ = difbuilderlib.new_marker_list() @@ -275,7 +271,20 @@ def get_offset(depsgraph, applymodifiers=True): off = [((maxv[i] - minv[i]) / 2) + 50 for i in range(0, 3)] return off + +def formatScale(scale): + return "%.5f %.5f %.5f" % (scale[0], scale[1], scale[2]) + + +def formatRotation(axis_ang): + from math import degrees + return "%.5f %.5f %.5f %.5f" % ( + axis_ang[0][0], + axis_ang[0][1], + axis_ang[0][2], + degrees(-axis_ang[1])) + class GamePathedInterior: def __init__(self, ob: Object, triggers: list[Object], offset, flip, double, usematnames, mbonly=True, bspmode="Fast", pointepsilon=1e-6, planeepsilon=1e-5, splitepsilon=1e-4): difbuilder = DifBuilder() @@ -345,7 +354,6 @@ def __init__(self, ob: Object, triggers: list[Object], offset, flip, double, use if (len(marker_ob.splines[0].bezier_points) != 0) else marker_ob.splines[0].points ) - msToNext = int(ob.dif_props.total_time / (len(marker_pts)-1)) path_type = ob.dif_props.marker_type if path_type == "linear": @@ -360,7 +368,36 @@ def __init__(self, ob: Object, triggers: list[Object], offset, flip, double, use if curve_obj: curve_transform = curve_obj.matrix_world + cum_times = [0] # Used for "target marker" triggers and "start index" + for index, pt in enumerate(marker_pts): + if index == len(marker_pts)-1: + msToNext = 0 + else: + if(ob.dif_props.constant_speed): + p0 = Vector(marker_pts[index].co[:3]) + p1 = Vector(marker_pts[index+1].co[:3]) + marker_dist = (p1 - p0).length + + if(ob.dif_props.marker_type == "spline"): + p0 = marker_pts[index-1].co[:3] + p1 = marker_pts[index].co[:3] + p2 = marker_pts[index+1].co[:3] + p3 = marker_pts[(index+2) % len(marker_pts)].co[:3] + length = GamePathedInterior.catmull_rom_length(p0, p1, p2, p3) + else: + length = marker_dist + + if(marker_dist < 0.01): + msToNext = ob.dif_props.pause_duration + else: + msToNext = length / (ob.dif_props.speed / 1000) + + else: + msToNext = ob.dif_props.total_time / (len(marker_pts)-1) + + msToNext = int(max(msToNext, 1)) + co = pt.co if len(co) == 4: co = Vector((co.x, co.y, co.z)) @@ -368,10 +405,9 @@ def __init__(self, ob: Object, triggers: list[Object], offset, flip, double, use if(curve_transform): co = curve_transform @ co - if index == len(marker_pts)-1: - marker_list.push_marker(co, 0, smoothing_type) - else: - marker_list.push_marker(co, msToNext, smoothing_type) + marker_list.push_marker(co, msToNext, smoothing_type) + + cum_times.append(cum_times[-1]+msToNext) else: marker_list.push_marker(ob.location, ob.dif_props.total_time, 0) @@ -379,6 +415,12 @@ def __init__(self, ob: Object, triggers: list[Object], offset, flip, double, use trigger_id_list = TriggerIDList() + if(ob.dif_props.constant_speed): + marker_idx = min(ob.dif_props.start_index, len(cum_times)-1) + starting_time = cum_times[marker_idx] + else: + starting_time = ob.dif_props.start_time + if(ob.dif_props.reverse): initial_target_position = -2 else: @@ -387,7 +429,13 @@ def __init__(self, ob: Object, triggers: list[Object], offset, flip, double, use for index, trigger in enumerate(triggers): if trigger.target_object is ob: trigger_id_list.push_trigger_id(index) - initial_target_position = 0 + initial_target_position = starting_time + + # Update the trigger target time if using "target marker" + if(trigger.target_marker): + marker_idx = min(trigger.target_index, len(cum_times)-1) + trigger.properties.add_kvp("targetTime", str(cum_times[marker_idx])) + trigger.name = "MustChange_m" + str(marker_idx) ob.to_mesh_clear() @@ -397,11 +445,40 @@ def __init__(self, ob: Object, triggers: list[Object], offset, flip, double, use propertydict = DIFDict() propertydict.add_kvp("initialTargetPosition", str(initial_target_position)) - propertydict.add_kvp("initialPosition", str(ob.dif_props.start_time)) + propertydict.add_kvp("initialPosition", str(starting_time)) + + if(ob.matrix_world != Matrix.Identity(4)): + propertydict.add_kvp("baseScale", formatScale(ob.scale)) + axis_ang_raw: Vector = ob.matrix_world.to_quaternion().to_axis_angle() + propertydict.add_kvp("baseRotation", formatRotation(axis_ang_raw)) self.properties = propertydict self.offset = [-(ob.location[i] + offset[i]) for i in range(0, 3)] + @staticmethod + def catmull_rom(t, p0, p1, p2, p3): + return 0.5 * ((3*p1 - 3*p2 + p3 - p0)*t*t*t + + (2*p0 - 5*p1 + 4*p2 - p3)*t*t + + (p2 - p0)*t + + 2*p1) + + @staticmethod + def catmull_rom_length(p0, p1, p2, p3, samples=20): + total_length = 0 + last_vec = None + + for i in range(0, samples+1): + t = i / samples + x = GamePathedInterior.catmull_rom(t, p0[0], p1[0], p2[0], p3[0]) + y = GamePathedInterior.catmull_rom(t, p0[1], p1[1], p2[1], p3[1]) + z = GamePathedInterior.catmull_rom(t, p0[2], p1[2], p2[2], p3[2]) + new_vec = Vector((x, y, z)) + if last_vec: + total_length += (new_vec - last_vec).length + last_vec = new_vec + + return total_length + class GameEntity: def __init__(self, ob, offset): @@ -411,18 +488,10 @@ def __init__(self, ob, offset): for prop in props.game_entity_properties: propertydict.add_kvp(prop.key, prop.value) - propertydict.add_kvp("scale", "%.5f %.5f %.5f" % (ob.scale[0], ob.scale[1], ob.scale[2])) + propertydict.add_kvp("scale", formatScale(ob.scale)) axis_ang_raw: Vector = ob.matrix_world.to_quaternion().to_axis_angle() - - #TODO fix or remove - from math import degrees - propertydict.add_kvp("rotation", "%.5f %.5f %.5f %.5f" % ( - axis_ang_raw[0][0], - axis_ang_raw[0][1], - axis_ang_raw[0][2], - degrees(axis_ang_raw[1])) - ) + propertydict.add_kvp("rotation", formatRotation(axis_ang_raw)) if props.game_entity_gameclass == "Trigger": propertydict.add_kvp("polyhedron", "0 0 0 1 0 0 0 -1 0 0 0 1") @@ -441,6 +510,9 @@ def __init__(self, ob, offset): for prop in props.game_entity_properties: propertydict.add_kvp(prop.key, prop.value) + #axis_ang_raw: Vector = ob.matrix_world.to_quaternion().to_axis_angle() + #propertydict.add_kvp("rotation", formatRotation(axis_ang_raw)) + self.position = [ob.location[i] + offset[i] for i in range(0, 3)] self.size = [ob.scale[0], -ob.scale[1], ob.scale[2]] self.datablock = props.game_entity_datablock @@ -448,6 +520,8 @@ def __init__(self, ob, offset): self.name = "MustChange" self.target_object = ob.dif_props.pathed_interior_target + self.target_marker = ob.dif_props.target_marker + self.target_index = ob.dif_props.target_index def save( diff --git a/blender_plugin/io_dif/import_dif.py b/blender_plugin/io_dif/import_dif.py index f9c3bc2..275108c 100644 --- a/blender_plugin/io_dif/import_dif.py +++ b/blender_plugin/io_dif/import_dif.py @@ -259,6 +259,7 @@ def load( itr.dif_props.interior_type = "pathed_interior" itr.dif_props.start_time = int(mover.properties.h.get("initialPosition", 0)) itr.dif_props.reverse = mover.properties.h.get("initialTargetPosition", 0) == "-2" + itr.dif_props.constant_speed = False waypoints: list[WayPoint] = mover.wayPoint @@ -299,6 +300,7 @@ def load( tobj.dif_props.interior_type = "path_trigger" tobj.dif_props.pathed_interior_target = itr tobj.dif_props.game_entity_datablock = trigger.datablock + tobj.dif_props.target_marker = False for key in trigger.properties.h: prop = tobj.dif_props.game_entity_properties.add() prop.key = key