mirror of
https://github.com/H-uru/korman.git
synced 2025-07-14 22:36:52 +00:00
Implement subworlds
This commit includes some bonus improvements for animated parenting situations that I discovered through sciencing the various MOUL and PotS PRPs. Hopefully it all works as advertised.
This commit is contained in:
@ -307,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:
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
||||
|
@ -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))
|
||||
|
@ -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
|
||||
|
@ -81,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",
|
||||
|
@ -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")
|
||||
|
@ -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")
|
||||
|
Reference in New Issue
Block a user