diff --git a/korman/exporter/explosions.py b/korman/exporter/explosions.py index 0b06071..715680b 100644 --- a/korman/exporter/explosions.py +++ b/korman/exporter/explosions.py @@ -23,6 +23,11 @@ class BlenderOptionNotSupportedError(ExportError): super(ExportError, self).__init__("Unsupported Blender Option: '{}'".format(opt)) +class ExportAssertionError(ExportError): + def __init__(self): + super(ExportError, self).__init__("Assertion failed") + + class TooManyUVChannelsError(ExportError): def __init__(self, obj, mat): msg = "There are too many UV Textures on the material '{}' associated with object '{}'.".format( diff --git a/korman/exporter/physics.py b/korman/exporter/physics.py index 69d5e89..a949b22 100644 --- a/korman/exporter/physics.py +++ b/korman/exporter/physics.py @@ -18,6 +18,7 @@ import mathutils from PyHSPlasma import * import weakref +from .explosions import ExportAssertionError from ..helpers import TemporaryObject from . import utils @@ -25,6 +26,17 @@ class PhysicsConverter: def __init__(self, exporter): self._exporter = weakref.ref(exporter) + def _convert_indices(self, mesh): + indices = [] + for face in mesh.tessfaces: + v = face.vertices + if len(v) == 3: + indices += v + elif len(v) == 4: + indices += (v[0], v[1], v[2],) + indices += (v[0], v[2], v[3],) + return indices + def _convert_mesh_data(self, bo, physical, indices=True): mesh = bo.to_mesh(bpy.context.scene, True, "RENDER", calc_tessface=False) mat = bo.matrix_world @@ -51,18 +63,50 @@ class PhysicsConverter: vertices = [hsVector3(i.co.x, i.co.y, i.co.z) for i in mesh.vertices] if indices: - indices = [] - for face in mesh.tessfaces: - v = face.vertices - if len(v) == 3: - indices += v - elif len(v) == 4: - indices += (v[0], v[1], v[2],) - indices += (v[0], v[2], v[3],) - return (vertices, indices) + return (vertices, self._convert_indices(mesh)) else: return vertices + def generate_flat_proxy(self, bo, so, z_coord=None, name=None): + """Generates a flat physical object""" + if so.sim is None: + if name is None: + name = bo.name + + simIface = self._mgr.add_object(pl=plSimulationInterface, bl=bo) + physical = self._mgr.add_object(pl=plGenericPhysical, bl=bo, name=name) + + simIface.physical = physical.key + physical.object = so.key + physical.sceneNode = self._mgr.get_scene_node(bl=bo) + + mesh = bo.to_mesh(bpy.context.scene, True, "RENDER", calc_tessface=False) + with TemporaryObject(mesh, bpy.data.meshes.remove) as mesh: + # We will apply all xform, seeing as how this is a special case... + mesh.transform(bo.matrix_world) + mesh.update(calc_tessface=True) + + if z_coord is None: + # Ensure all vertices are coplanar + z_coords = [i.co.z for i in mesh.vertices] + delta = max(z_coords) - min(z_coords) + if delta > 0.0002: + raise ExportAssertionError() + vertices = [hsVector3(i.co.x, i.co.y, i.co.z) for i in mesh.vertices] + else: + # Flatten out all points to the given Z-coordinate + vertices = [hsVector3(i.co.x, i.co.y, z_coord) for i in mesh.vertices] + physical.verts = vertices + physical.indices = self._convert_indices(mesh) + physical.boundsType = plSimDefs.kProxyBounds + else: + simIface = so.sim.object + physical = simIface.physical.object + if name is not None: + physical.key.name = name + + return (simIface, physical) + def generate_physical(self, bo, so, bounds, name=None): """Generates a physical object for the given object pair""" if so.sim is None: diff --git a/korman/properties/modifiers/water.py b/korman/properties/modifiers/water.py index 2b41733..0424e3d 100644 --- a/korman/properties/modifiers/water.py +++ b/korman/properties/modifiers/water.py @@ -19,7 +19,157 @@ import math from PyHSPlasma import * from .base import PlasmaModifierProperties -from ...exporter import ExportError +from ...exporter import ExportError, ExportAssertionError + +class PlasmaSwimRegion(PlasmaModifierProperties, bpy.types.PropertyGroup): + pl_id = "swimregion" + + bl_category = "Water" + bl_label = "Swimming Surface" + bl_description = "Surface that the avatar can swim on" + bl_icon = "MOD_WAVE" + + _CURRENTS = { + "NONE": plSwimRegionInterface, + "CIRCULAR": plSwimCircularCurrentRegion, + "STRAIGHT": plSwimStraightCurrentRegion, + } + + region_name = StringProperty(name="Region", + description="Swimming detector region", + options=set()) + + down_buoyancy = FloatProperty(name="Downward Buoyancy", + description="Distance the avatar sinks into the water", + min=0.0, max=100.0, default=3.0, + options=set()) + up_buoyancy = FloatProperty(name="Up Buoyancy", + description="Distance the avatar rises up after sinking", + min=0.0, max=100.0, default=0.05, + options=set()) + up_velocity = FloatProperty(name="Up Velcocity", + description="Rate at which the avatar rises", + min=0.0, max=100.0, default=3.0, + options=set()) + + current_type = EnumProperty(name="Water Current", + description="", + items=[("NONE", "None", "No current"), + ("CIRCULAR", "Circular", "Circular current"), + ("STRAIGHT", "Straight", "Straight current")], + options=set()) + rotation = FloatProperty(name="Rotation", + description="Rate of rotation about the current object", + min=-100.0, max=100.0, default=1.0, + options=set()) + near_distance = FloatProperty(name="Near Distance", + description="Maximum distance at which the current is at the Near Velocity rate", + min=0.0, max=10000.0, default=1.0, + options=set()) + far_distance = FloatProperty(name="Far Distance", + description="Distance at which the current is at the Far Velocity rate", + min=0.0, max=10000.0, default=1.0, + options=set()) + near_velocity = FloatProperty(name="Near Velocity", + description="Current velocity near the region center", + min=-100.0, max=100.0, default=0.0, + options=set()) + far_velocity = FloatProperty(name="Far Velocity", + description="Current velocity far from the region center", + min=-100.0, max=100.0, default=0.0, + options=set()) + current_object = StringProperty(name="Current Object", + description="Object whose Y-axis defines the direction of the current", + options=set()) + + def export(self, exporter, bo, so): + swimIface = self.get_key(exporter, so).object + swimIface.downBuoyancy = self.down_buoyancy + swimIface.upBuoyancy = self.up_buoyancy + swimIface.maxUpwardVel = self.up_velocity + if isinstance(swimIface, plSwimCircularCurrentRegion): + swimIface.rotation = self.rotation + swimIface.pullNearDistSq = pow(self.near_distance, 2) + swimIface.pullFarDistSq = pow(self.far_distance, 2) + swimIface.pullNearVel = self.near_velocity + swimIface.pullFarVel = self.far_velocity + elif isinstance(swimIface, plSwimStraightCurrentRegion): + swimIface.nearDist = self.near_distance + swimIface.farDist = self.far_distance + swimIface.nearVel = self.near_velocity + swimIface.farVel = self.far_velocity + if isinstance(swimIface, (plSwimCircularCurrentRegion, plSwimStraightCurrentRegion)): + if not self.current_object: + raise ExportError("Swimming Surface '{}' does not specify a current object".format(bo.name)) + current_bo = bpy.data.objects.get(self.current_object, None) + if current_bo is None: + raise ExportError("Swimming Surface '{}' specifies an invalid current object '{}'".format(bo.name, self.current_object)) + swimIface.currentObj = exporter.mgr.find_create_key(plSceneObject, bl=current_bo) + + # The surface needs bounds for LOS -- this is generally a flat plane, or I would think... + # NOTE: If the artist has this on a WaveSet, they probably intend for the avatar to swim on + # the surface of the water BUT wave sets are supposed to conform to the bottom of the + # pool. Therefore, we need to flatten out a temporary mesh in that case. + # Ohey! CWE doesn't let you swim at all if the surface isn't flat... + swim_phys_name = "{}_SwimSurfaceLOS".format(bo.name) + if bo.plasma_modifiers.water_basic.enabled: + simIface, physical = exporter.physics.generate_flat_proxy(bo, so, bo.location[2], swim_phys_name) + else: + try: + simIface, physical = exporter.physics.generate_flat_proxy(bo, so, None, swim_phys_name) + except ExportAssertionError: + raise ExportError("Swimming Surface '{}' must be flat".format(bo.name)) + physical.LOSDBs |= plSimDefs.kLOSDBSwimRegion + + # Detector region bounds + if self.region_name: + region_bo = bpy.data.objects.get(self.region_name, None) + if region_bo is None: + raise ExportError("Swim Surface '{}' references invalid region '{}'".format(bo.name, self.region_name)) + region_so = exporter.mgr.find_create_object(plSceneObject, bl=region_bo) + + # Good news: if this phys has already been exported, this is basically a noop + det_name = "{}_SwimDetector".format(self.region_name) + bounds = region_bo.plasma_modifiers.collision.bounds + simIface, physical = exporter.physics.generate_physical(region_bo, region_so, bounds, det_name) + physical.memberGroup = plSimDefs.kGroupDetector + physical.reportGroup |= 1 << plSimDefs.kGroupAvatar + + # I am a little concerned if we already have a plSwimDetector... I am not certain how + # well Plasma would tolerate having a plSwimMsg with multiple regions referenced. + # If you're brave, maybe you will test this... + # What? Me test it? + # I are chicken. + # Mmmmm chicken ***drool*** + if exporter.mgr.find_key(plSwimDetector, name=det_name, so=region_so) is None: + enter_msg, exit_msg = plSwimMsg(), plSwimMsg() + for i in (enter_msg, exit_msg): + i.BCastFlags = plMessage.kLocalPropagate | plMessage.kPropagateToModifiers + i.sender = region_so.key + i.swimRegion = swimIface.key + enter_msg.isEntering = True + exit_msg.isEntering = False + + detector = exporter.mgr.add_object(plSwimDetector, name=det_name, so=region_so) + detector.enterMsg = enter_msg + detector.exitMsg = exit_msg + else: + # OK, I lied. It is perfectly legal to NOT have a detector... Think about Ahnonay, + # if you want to have currents inside of a large body of water, only your main + # swimming surface should have a detector. m'kay? But still, we might want to make note + # of this sitation. Just in case someone is like "WTF! Why am I not swimming?!?!1111111" + # Because you need to have a detector, dummy. + exporter.report.warn("Swimming Surface '{}' does not specify a detector region".format(bo.name), indent=2) + + def get_key(self, exporter, so=None): + pClass = self._CURRENTS[self.current_type] + return exporter.mgr.find_create_key(pClass, bl=self.id_data, so=so) + + def harvest_actors(self): + if self.current_type != "NONE" and self.current_object: + return set((self.current_object,)) + return set() + class PlasmaWaterModifier(PlasmaModifierProperties, bpy.types.PropertyGroup): pl_id = "water_basic" diff --git a/korman/ui/modifiers/water.py b/korman/ui/modifiers/water.py index 886e99f..1b9d63c 100644 --- a/korman/ui/modifiers/water.py +++ b/korman/ui/modifiers/water.py @@ -15,6 +15,45 @@ import bpy +def swimregion(modifier, layout, context): + split = layout.split() + col = split.column() + col.label("Detector Region:") + col.prop_search(modifier, "region_name", bpy.data, "objects", text="") + + region_bo = bpy.data.objects.get(modifier.region_name, None) + col = split.column() + col.enabled = region_bo is not None + bounds_src = region_bo if region_bo is not None else modifier.id_data + col.label("Detector Bounds:") + col.prop(bounds_src.plasma_modifiers.collision, "bounds", text="") + + split = layout.split() + col = split.column(align=True) + col.label("Buoyancy:") + col.prop(modifier, "down_buoyancy", text="Down") + col.prop(modifier, "up_buoyancy", text="Up") + + col = split.column() + col.label("Current:") + col.prop(modifier, "current_type", text="") + if modifier.current_type == "CIRCULAR": + col.prop(modifier, "rotation") + + if modifier.current_type != "NONE": + split = layout.split() + col = split.column(align=True) + col.label("Distance:") + col.prop(modifier, "near_distance", text="Near") + col.prop(modifier, "far_distance", text="Far") + + col = split.column(align=True) + col.label("Velocity:") + col.prop(modifier, "near_velocity", text="Near") + col.prop(modifier, "far_velocity", text="Far") + + layout.prop_search(modifier, "current_object", bpy.data, "objects") + def water_basic(modifier, layout, context): layout.prop_search(modifier, "wind_object_name", bpy.data, "objects") layout.prop_search(modifier, "envmap_name", bpy.data, "textures")