diff --git a/korman/exporter/convert.py b/korman/exporter/convert.py index edf378e..8bf5da0 100644 --- a/korman/exporter/convert.py +++ b/korman/exporter/convert.py @@ -191,7 +191,8 @@ class Exporter: if so is None: so = self.mgr.find_create_object(plSceneObject, bl=bl) if so.coord is None: - ci = self.mgr.add_object(plCoordinateInterface, bl=bl, so=so) + ci_cls = bl.plasma_object.ci_type + ci = self.mgr.add_object(ci_cls, bl=bl, so=so) # Now we have the "fun" work of filling in the CI ci.localToWorld = utils.matrix44(bl.matrix_basis) @@ -306,6 +307,7 @@ class Exporter: self.report.progress_advance() self.report.progress_range = len(self._objects) inc_progress = self.report.progress_increment + self.report.msg("\nPost-Processing SceneObjects...") mat_mgr = self.mesh.material for bl_obj in self._objects: diff --git a/korman/exporter/physics.py b/korman/exporter/physics.py index 90fd367..1c67529 100644 --- a/korman/exporter/physics.py +++ b/korman/exporter/physics.py @@ -18,10 +18,15 @@ import mathutils from PyHSPlasma import * import weakref -from .explosions import ExportAssertionError +from .explosions import ExportError, ExportAssertionError from ..helpers import TemporaryObject from . import utils +def _set_phys_prop(prop, sim, phys, value=True): + """Sets properties on plGenericPhysical and plSimulationInterface (seeing as how they are duped)""" + sim.setProperty(prop, value) + phys.setProperty(prop, value) + class PhysicsConverter: def __init__(self, exporter): self._exporter = weakref.ref(exporter) @@ -123,6 +128,50 @@ class PhysicsConverter: physical.object = so.key physical.sceneNode = self._mgr.get_scene_node(bl=bo) + # Got subworlds? + subworld = bo.plasma_object.subworld + if self.is_dedicated_subworld(subworld, sanity_check=False): + physical.subWorld = self._mgr.find_create_key(plHKSubWorld, bl=subworld) + + # Ensure this thing is set up properly for animations. + # This was previously the collision modifier's postexport method, but that + # would miss cases where we have animated detectors (subworlds!!!) + def _iter_object_tree(bo, stop_at_subworld): + while bo is not None: + if stop_at_subworld and self.is_dedicated_subworld(bo, sanity_check=False): + return + yield bo + bo = bo.parent + + ver = self._mgr.getVer() + for i in _iter_object_tree(bo, ver == pvMoul): + if i.plasma_object.has_transform_animation: + tree_xformed = True + break + else: + tree_xformed = False + + if tree_xformed: + bo_xformed = bo.plasma_object.has_transform_animation + + # MOUL: only objects that have animation data are kPhysAnim + if ver != pvMoul or bo_xformed: + _set_phys_prop(plSimulationInterface.kPhysAnim, simIface, physical) + # PotS: objects inheriting parent animation only are not pinned + # MOUL: animated objects in subworlds are not pinned + if bo_xformed and (ver != pvMoul or subworld is None): + _set_phys_prop(plSimulationInterface.kPinned, simIface, physical) + # MOUL: child objects are kPassive + if ver == pvMoul and bo.parent is not None: + _set_phys_prop(plSimulationInterface.kPassive, simIface, physical) + # FilterCoordinateInterfaces are kPassive + if bo.plasma_object.ci_type == plFilterCoordInterface: + _set_phys_prop(plSimulationInterface.kPassive, simIface, physical) + + # If the mass is zero, then we will fail to animate. Fix that. + if physical.mass == 0.0: + physical.mass = 1.0 + getattr(self, "_export_{}".format(bounds))(bo, physical) else: simIface = so.sim.object @@ -176,6 +225,18 @@ class PhysicsConverter: physical.verts = vertices physical.indices = indices + def is_dedicated_subworld(self, bo, sanity_check=True): + """Determines if a subworld object defines an alternate physics world""" + if bo is None: + return False + subworld_mod = bo.plasma_modifiers.subworld_def + if not subworld_mod.enabled: + if sanity_check: + raise ExportError("'{}' is not a subworld".format(bo.name)) + else: + return False + return subworld_mod.is_dedicated_subworld(self._exporter()) + @property def _mgr(self): return self._exporter().mgr diff --git a/korman/idprops.py b/korman/idprops.py index 45a6fab..1423943 100644 --- a/korman/idprops.py +++ b/korman/idprops.py @@ -133,6 +133,9 @@ def poll_mesh_objects(self, value): def poll_softvolume_objects(self, value): return value.plasma_modifiers.softvolume.enabled +def poll_subworld_objects(self, value): + return value.plasma_modifiers.subworld_def.enabled + def poll_visregion_objects(self, value): return value.plasma_modifiers.visregion.enabled diff --git a/korman/properties/modifiers/__init__.py b/korman/properties/modifiers/__init__.py index 7713ed9..294af4c 100644 --- a/korman/properties/modifiers/__init__.py +++ b/korman/properties/modifiers/__init__.py @@ -77,10 +77,19 @@ def modifier_mapping(): d = {} sorted_modifiers = sorted(PlasmaModifierProperties.__subclasses__(), key=lambda x: x.bl_label) for i, mod in enumerate(sorted_modifiers): + pl_id, category, label, description = mod.pl_id, mod.bl_category, mod.bl_label, mod.bl_description icon = getattr(mod, "bl_icon", "") - tup = (mod.pl_id, mod.bl_label, mod.bl_description, icon, i) - if mod.bl_category not in d: - d[mod.bl_category] = [tup] + + # The modifier might include the cateogry name in its name, so we'll strip that. + if label != category: + if label.startswith(category): + label = label[len(category)+1:] + if label.endswith(category): + label = label[:-len(category)-1] + + tup = (pl_id, label, description, icon, i) + if category not in d: + d[category] = [tup] else: - d[mod.bl_category].append(tup) + d[category].append(tup) return d diff --git a/korman/properties/modifiers/anim.py b/korman/properties/modifiers/anim.py index 341e9cd..9821636 100644 --- a/korman/properties/modifiers/anim.py +++ b/korman/properties/modifiers/anim.py @@ -104,6 +104,49 @@ class AnimGroupObject(idprops.IDPropObjectMixin, bpy.types.PropertyGroup): return {"child_anim": "object_name"} +class PlasmaAnimationFilterModifier(PlasmaModifierProperties): + pl_id = "animation_filter" + + bl_category = "Animation" + bl_label = "Filter Transform" + bl_description = "Filter animation components" + bl_icon = "UNLINKED" + + no_rotation = BoolProperty(name="Filter Rotation", + description="Filter rotations", + options=set()) + + no_transX = BoolProperty(name="Filter X Translation", + description="Filter the X component of translations", + options=set()) + no_transY = BoolProperty(name="Filter Y Translation", + description="Filter the Y component of translations", + options=set()) + no_transZ = BoolProperty(name="Filter Z Translation", + description="Filter the Z component of translations", + options=set()) + + def export(self, exporter, bo, so): + # By this point, the object should already have a plFilterCoordInterface + # created by the converter. Let's test that assumption. + coord = so.coord.object + assert isinstance(coord, plFilterCoordInterface) + + # Apply filtercoordinterface properties + if self.no_rotation: + coord.filterMask |= plFilterCoordInterface.kNoRotation + if self.no_transX: + coord.filterMask |= plFilterCoordInterface.kNoTransX + if self.no_transY: + coord.filterMask |= plFilterCoordInterface.kNoTransY + if self.no_transZ: + coord.filterMask |= plFilterCoordInterface.kNoTransZ + + @property + def requires_actor(self): + return True + + class PlasmaAnimationGroupModifier(ActionModifier, PlasmaModifierProperties): pl_id = "animation_group" pl_depends = {"animation"} diff --git a/korman/properties/modifiers/physics.py b/korman/properties/modifiers/physics.py index aa983a3..e84c699 100644 --- a/korman/properties/modifiers/physics.py +++ b/korman/properties/modifiers/physics.py @@ -18,6 +18,7 @@ from bpy.props import * from PyHSPlasma import * from .base import PlasmaModifierProperties +from ...exporter import ExportError # These are the kinds of physical bounds Plasma can work with. # This sequence is acceptable in any EnumProperty @@ -84,32 +85,6 @@ class PlasmaCollider(PlasmaModifierProperties): if self.terrain: physical.LOSDBs |= plSimDefs.kLOSDBAvatarWalkable - def _make_physical_movable(self, so): - sim = so.sim - if sim is not None: - sim = sim.object - phys = sim.physical.object - _set_phys_prop(plSimulationInterface.kPhysAnim, sim, phys) - - # If the mass is zero, then we will fail to animate. Fix that. - if phys.mass == 0.0: - phys.mass = 1.0 - - # set kPinned so it doesn't fall through - _set_phys_prop(plSimulationInterface.kPinned, sim, phys) - - # Do the same for child objects - for child in so.coord.object.children: - self._make_physical_movable(child.object) - - def post_export(self, exporter, bo, so): - test_bo = bo - while test_bo is not None: - if test_bo.plasma_object.has_transform_animation: - self._make_physical_movable(so) - break - test_bo = test_bo.parent - @property def key_name(self): return "{}_Collision".format(self.id_data.name) @@ -117,3 +92,56 @@ class PlasmaCollider(PlasmaModifierProperties): @property def requires_actor(self): return self.dynamic + + +class PlasmaSubworld(PlasmaModifierProperties): + pl_id = "subworld_def" + + bl_category = "Physics" + bl_label = "Subworld" + bl_description = "Subworld definition" + bl_icon = "WORLD" + + sub_type = EnumProperty(name="Subworld Type", + description="Specifies the physics strategy to use for this subworld", + items=[("auto", "Auto", "Korman will decide which physics strategy to use"), + ("dynamicav", "Dynamic Avatar", "Allows the avatar to affected by dynamic physicals"), + ("subworld", "Separate World", "Causes all objects to be placed in a separate physics simulation")], + default="auto", + options=set()) + gravity = FloatVectorProperty(name="Gravity", + description="Subworld's gravity defined in feet per second squared", + size=3, default=(0.0, 0.0, -32.174), precision=3, + subtype="ACCELERATION", unit="ACCELERATION") + + def export(self, exporter, bo, so): + if self.is_dedicated_subworld(exporter): + # NOTE to posterity... Cyan's PotS/Havok subworlds appear to have a + # plHKPhysical object that is set as LOSOnly convex hull. They appear to + # be a bounding box. PyPRP generated PRPs do not do this and work just fine, + # however, so this is probably just a quirk of the Havok-era PlasmaMAX + subworld = exporter.mgr.find_create_object(plHKSubWorld, so=so) + subworld.gravity = hsVector3(*self.gravity) + + def is_dedicated_subworld(self, exporter): + if exporter.mgr.getVer() != pvMoul: + return True + if self.sub_type == "subworld": + return True + elif self.sub_type == "dynamicav": + return False + else: + return not self.property_unset("gravity") + + def post_export(self, exporter, bo, so): + # It appears PotS does something really fancy with subworlds under the hood such that + # if you make a subworld that has collision, it will get into an infinite loop in + # plCoordinateInterface::IGetRoot. Not really sure why this happens (nor do I care), + # but we definitely don't want it to happen. + if bo.type != "EMPTY": + exporter.report.warn("Subworld '{}' is attached to a '{}'--this should be an empty.", bo.name, bo.type, indent=1) + if so.sim: + if exporter.mgr.getVer() > pvPots: + exporter.report.port("Subworld '{}' has physics data--this will cause PotS to crash.", bo.name, indent=1) + else: + raise ExportError("Subworld '{}' cannot have physics data (should be an empty).".format(bo.name)) diff --git a/korman/properties/modifiers/region.py b/korman/properties/modifiers/region.py index f63ab44..622f1c0 100644 --- a/korman/properties/modifiers/region.py +++ b/korman/properties/modifiers/region.py @@ -17,7 +17,7 @@ import bpy from bpy.props import * from PyHSPlasma import * -from ...exporter import ExportError +from ...exporter import ExportError, ExportAssertionError from ...helpers import TemporaryObject from ... import idprops @@ -240,3 +240,72 @@ class PlasmaSoftVolume(idprops.IDPropMixin, PlasmaModifierProperties): def _idprop_sources(self): return {"node_tree_name": bpy.data.node_groups} + + +class PlasmaSubworldRegion(PlasmaModifierProperties): + pl_id = "subworld_rgn" + + bl_category = "Region" + bl_label = "Subworld Region" + bl_description = "Subworld transition region" + + subworld = PointerProperty(name="Subworld", + description="Subworld to transition into", + type=bpy.types.Object, + poll=idprops.poll_subworld_objects) + transition = EnumProperty(name="Transition", + description="When to transition to the new subworld", + items=[("enter", "On Enter", "Transition when the avatar enters the region"), + ("exit", "On Exit", "Transition when the avatar exits the region")], + default="enter", + options=set()) + + def export(self, exporter, bo, so): + # Due to the fact that our subworld modifier can produce both RidingAnimatedPhysical + # and [HK|PX]Subworlds depending on the situation, this could get hairy, fast. + # Start by surveying the lay of the land. + from_sub, to_sub = bo.plasma_object.subworld, self.subworld + from_isded = exporter.physics.is_dedicated_subworld(from_sub) + to_isded = exporter.physics.is_dedicated_subworld(to_sub) + if 1: + def get_log_text(bo, isded): + main = "[Main World]" if bo is None else bo.name + sub = "Subworld" if isded or bo is None else "RidingAnimatedPhysical" + return main, sub + from_name, from_type = get_log_text(from_sub, from_isded) + to_name, to_type = get_log_text(to_sub, to_isded) + exporter.report.msg("Transition from '{}' ({}) to '{}' ({})", + from_name, from_type, to_name, to_type, + indent=2) + + # I think the best solution here is to not worry about the excitement mentioned above. + # If we encounter anything truly interesting, we can fix it in CWE more easily IMO because + # the game actually knows more about the avatar's state than we do here in the exporter. + if to_isded or (from_isded and to_sub is None): + region = exporter.mgr.find_create_object(plSubworldRegionDetector, so=so) + if to_sub is not None: + region.subworld = exporter.mgr.find_create_key(plSceneObject, bl=to_sub) + region.onExit = self.transition == "exit" + else: + msg = plRideAnimatedPhysMsg() + msg.BCastFlags |= plMessage.kLocalPropagate | plMessage.kPropagateToModifiers + msg.sender = so.key + msg.entering = to_sub is not None + + # In Cyan's PlasmaMAX RAP detector, it acts as more of a traditional region + # that changes us over to a dynamic character controller on region enter and + # reverts on region exit. We're going for an approach that is backwards compatible + # with subworlds, so our enter/exit regions are separate. Here, enter/exit message + # corresponds with when we should trigger the transition. + region = exporter.mgr.find_create_object(plRidingAnimatedPhysicalDetector, so=so) + if self.transition == "enter": + region.enterMsg = msg + elif self.transition == "exit": + region.exitMsg = msg + else: + raise ExportAssertionError() + + # Fancy pants region collider type shit + simIface, physical = exporter.physics.generate_physical(bo, so, self.id_data.plasma_modifiers.collision.bounds, self.key_name) + physical.memberGroup = plSimDefs.kGroupDetector + physical.reportGroup |= 1 << plSimDefs.kGroupAvatar diff --git a/korman/properties/prop_object.py b/korman/properties/prop_object.py index 2ea9e55..e9de5e9 100644 --- a/korman/properties/prop_object.py +++ b/korman/properties/prop_object.py @@ -52,6 +52,13 @@ class PlasmaObject(bpy.types.PropertyGroup): default=False, options={"HIDDEN"}) + @property + def ci_type(self): + if self.id_data.plasma_modifiers.animation_filter.enabled: + return plFilterCoordInterface + else: + return plCoordinateInterface + @property def has_animation_data(self): bo = self.id_data @@ -74,6 +81,16 @@ class PlasmaObject(bpy.types.PropertyGroup): return {"location", "rotation_euler", "scale"} & data_paths return False + @property + def subworld(self): + bo = self.id_data + while bo is not None: + if bo.plasma_modifiers.subworld_def.enabled: + return bo + else: + bo = bo.parent + return None + class PlasmaNet(bpy.types.PropertyGroup): manual_sdl = BoolProperty(name="Override SDL", diff --git a/korman/ui/modifiers/anim.py b/korman/ui/modifiers/anim.py index a17f0c4..681e43a 100644 --- a/korman/ui/modifiers/anim.py +++ b/korman/ui/modifiers/anim.py @@ -42,6 +42,18 @@ def animation(modifier, layout, context): col.prop_search(modifier, "loop_start", action, "pose_markers", icon="PMARKER") col.prop_search(modifier, "loop_end", action, "pose_markers", icon="PMARKER") +def animation_filter(modifier, layout, context): + split = layout.split() + + col = split.column() + col.label("Translation:") + col.prop(modifier, "no_transX", text="Filter X") + col.prop(modifier, "no_transY", text="Filter Y") + col.prop(modifier, "no_transZ", text="Filter Z") + + col = split.column() + col.label("Rotation:") + col.prop(modifier, "no_rotation", text="Filter Rotation") class GroupListUI(bpy.types.UIList): def draw_item(self, context, layout, data, item, icon, active_data, active_property, index=0, flt_flag=0): diff --git a/korman/ui/modifiers/physics.py b/korman/ui/modifiers/physics.py index fc72225..615e476 100644 --- a/korman/ui/modifiers/physics.py +++ b/korman/ui/modifiers/physics.py @@ -38,3 +38,8 @@ def collision(modifier, layout, context): col = split.column() col.active = modifier.dynamic col.prop(modifier, "mass") + +def subworld_def(modifier, layout, context): + layout.prop(modifier, "sub_type") + if modifier.sub_type != "dynamicav": + layout.prop(modifier, "gravity") diff --git a/korman/ui/modifiers/region.py b/korman/ui/modifiers/region.py index ea82bd6..58b6b23 100644 --- a/korman/ui/modifiers/region.py +++ b/korman/ui/modifiers/region.py @@ -39,3 +39,9 @@ def softvolume(modifier, layout, context): col = split.column() col.prop(modifier, "invert") col.prop(modifier, "soft_distance") + +def subworld_rgn(modifier, layout, context): + layout.prop(modifier, "subworld") + collision_mod = modifier.id_data.plasma_modifiers.collision + layout.prop(collision_mod, "bounds") + layout.prop(modifier, "transition")