Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 9 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
@@ -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

Expand Down Expand Up @@ -75,18 +75,24 @@ 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.
- Properties: a list of additional key value pairs which will be set to the object on Create Subs
- 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

Expand Down
63 changes: 45 additions & 18 deletions blender_plugin/io_dif/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -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=(
Expand All @@ -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=(
Expand All @@ -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"
Expand All @@ -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?

Expand All @@ -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()
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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)
Expand Down
118 changes: 96 additions & 22 deletions blender_plugin/io_dif/export_dif.py
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down Expand Up @@ -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()
Expand Down Expand Up @@ -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()
Expand Down Expand Up @@ -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":
Expand All @@ -360,25 +368,59 @@ 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))

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)
marker_list.push_marker(ob.location, 0, 0)

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:
Expand All @@ -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()

Expand All @@ -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):
Expand All @@ -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")
Expand All @@ -441,13 +510,18 @@ 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
self.properties = propertydict
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(
Expand Down
2 changes: 2 additions & 0 deletions blender_plugin/io_dif/import_dif.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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
Expand Down