# This file is part of Korman. # # Korman is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # Korman is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with Korman. If not, see . import bmesh import bpy import itertools import mathutils from PyHSPlasma import * import weakref from .explosions import ExportError, ExportAssertionError from ..helpers import bmesh_from_object, 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) self._bounds_converters = { "box": self._export_box, "sphere": self._export_sphere, "hull": self._export_hull, "trimesh": self._export_trimesh, } def _apply_props(self, simIface, physical, props): for i in props.get("properties", []): _set_phys_prop(getattr(plSimulationInterface, i), simIface, physical) for i in props.get("losdbs", []): physical.LOSDBs |= getattr(plSimDefs, i) for i in props.get("report_groups", []): physical.reportGroup |= 1 << getattr(plSimDefs, i) for i in props.get("collide_groups", []): physical.collideGroup |= 1 << getattr(plSimDefs, i) 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, local_space, mat, indices=True): mesh = bo.to_mesh(bpy.context.scene, True, "RENDER", calc_tessface=False) with TemporaryObject(mesh, bpy.data.meshes.remove): if local_space: mesh.update(calc_tessface=indices) physical.pos = hsVector3(*mat.to_translation()) physical.rot = utils.quaternion(mat.to_quaternion()) # Physicals can't have scale... scale = mat.to_scale() if scale[0] == 1.0 and scale[1] == 1.0 and scale[2] == 1.0: # Whew, don't need to do any math! vertices = [hsVector3(*i.co) for i in mesh.vertices] else: # Dagnabbit... vertices = [hsVector3(i.co.x * scale.x, i.co.y * scale.y, i.co.z * scale.z) for i in mesh.vertices] else: # apply the transform to the physical itself utils.transform_mesh(mesh, mat) mesh.update(calc_tessface=indices) vertices = [hsVector3(*i.co) for i in mesh.vertices] if indices: return (vertices, self._convert_indices(mesh)) else: return vertices def generate_flat_proxy(self, bo, so, **kwargs): """Generates a flat physical object""" z_coord = kwargs.pop("z_coord", None) if so.sim is None: simIface = self._mgr.add_object(pl=plSimulationInterface, bl=bo) physical = self._mgr.add_object(pl=plGenericPhysical, bl=bo) 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): # No mass and no emedded xform, so we force worldspace collision. utils.transform_mesh(mesh, 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) 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 group_name = kwargs.get("member_group") if group_name: physical.memberGroup = getattr(plSimDefs, group_name) else: simIface = so.sim.object physical = simIface.physical.object member_group = getattr(plSimDefs, kwargs.get("member_group", "kGroupLOSOnly")) if physical.memberGroup != member_group and member_group != plSimDefs.kGroupLOSOnly: self._report.warn("{}: Physical memberGroup overwritten!", bo.name) physical.memberGroup = member_group self._apply_props(simIface, physical, kwargs) def generate_physical(self, bo, so, **kwargs): """Generates a physical object for the given object pair. The following optional arguments are allowed: - bounds: (defaults to collision modifier setting) - member_group: str attribute of plSimDefs, defaults to kGroupStatic NOTE that kGroupLOSOnly generation will only succeed if no one else has generated this physical in another group - properties: sequence of str bit names from plSimulationInterface - losdbs: sequence of str bit names from plSimDefs - report_groups: sequence of str bit names from plSimDefs - collide_groups: sequence of str bit names from plSimDefs """ if so.sim is None: simIface = self._mgr.add_object(pl=plSimulationInterface, bl=bo) physical = self._mgr.add_object(pl=plGenericPhysical, bl=bo) ver = self._mgr.getVer() simIface.physical = physical.key 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) # Export the collision modifier here since we like stealing from it anyway. mod = bo.plasma_modifiers.collision bounds = kwargs.get("bounds", mod.bounds) if mod.enabled: physical.friction = mod.friction physical.restitution = mod.restitution if mod.dynamic: if ver <= pvPots: physical.collideGroup = (1 << plSimDefs.kGroupDynamic) | \ (1 << plSimDefs.kGroupStatic) physical.memberGroup = plSimDefs.kGroupDynamic physical.mass = mod.mass _set_phys_prop(plSimulationInterface.kStartInactive, simIface, physical, value=mod.start_asleep) elif not mod.avatar_blocker: physical.memberGroup = plSimDefs.kGroupLOSOnly else: physical.memberGroup = plSimDefs.kGroupStatic # Line of Sight DB if mod.camera_blocker: physical.LOSDBs |= plSimDefs.kLOSDBCameraBlockers _set_phys_prop(plSimulationInterface.kCameraAvoidObject, simIface, physical) if mod.terrain: physical.LOSDBs |= plSimDefs.kLOSDBAvatarWalkable # Hacky? We'd like to share the simple surface descriptors(TM) as much as possible... # This could result in a few orphaned PhysicalSndGroups, but I think that's preferable # to having a bunch of empty objects...? if mod.surface != "kNone": sndgroup = self._mgr.find_create_object(plPhysicalSndGroup, so=so, name="SURFACEGEN_{}".format(mod.surface)) sndgroup.group = getattr(plPhysicalSndGroup, mod.surface) physical.soundGroup = sndgroup.key else: group_name = kwargs.get("member_group") if group_name: physical.memberGroup = getattr(plSimDefs, group_name) # 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 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 # Always pin these objects - otherwise they may start falling through the floor. # Unless you've marked it kickable... if not mod.dynamic: _set_phys_prop(plSimulationInterface.kPinned, simIface, physical) # MOUL: only objects that have animation data are kPhysAnim if ver != pvMoul or bo_xformed: _set_phys_prop(plSimulationInterface.kPhysAnim, simIface, physical) # Any physical that is parented by not kickable (dynamic) is passive - # meaning we don't need to report back any changes from physics. Same for # plFilterCoordInterface, which filters out some axes. if (bo.parent is not None and not mod.dynamic) or 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 # Different Plasma versions have different ways they expect to get physical transforms. # With Havok, massless objects are in absolute worldspace while massed (movable) objects # are in object-local space. # In PhysX, objects with a coordinate interface are in local to SUBWORLD space, otherwise # they are in absolute worldspace. if ver <= pvPots: local_space, mat = physical.mass > 0.0, bo.matrix_world elif ver == pvMoul: if self._exporter().has_coordiface(bo): local_space = True mat = subworld.matrix_world.inverted() * bo.matrix_world if subworld else bo.matrix_world else: local_space, mat = False, bo.matrix_world else: raise NotImplementedError("ODE physical transform") self._bounds_converters[bounds](bo, physical, local_space, mat) else: simIface = so.sim.object physical = simIface.physical.object member_group = getattr(plSimDefs, kwargs.get("member_group", "kGroupLOSOnly")) if physical.memberGroup != member_group and member_group != plSimDefs.kGroupLOSOnly: self._report.warn("{}: Physical memberGroup overwritten!", bo.name, indent=2) physical.memberGroup = member_group self._apply_props(simIface, physical, kwargs) def _export_box(self, bo, physical, local_space, mat): """Exports box bounds based on the object""" physical.boundsType = plSimDefs.kBoxBounds vertices = self._convert_mesh_data(bo, physical, local_space, mat, indices=False) physical.calcBoxBounds(vertices) def _export_hull(self, bo, physical, local_space, mat): """Exports convex hull bounds based on the object""" physical.boundsType = plSimDefs.kHullBounds # Only certain builds of libHSPlasma are able to take artist generated triangle soups and # bake them to convex hulls. Specifically, Windows 32-bit w/PhysX 2.6. Everything else just # needs to have us provide some friendlier data... with bmesh_from_object(bo) as mesh: if local_space: physical.pos = hsVector3(*mat.to_translation()) physical.rot = utils.quaternion(mat.to_quaternion()) bmesh.ops.scale(mesh, vec=mat.to_scale(), verts=mesh.verts) else: mesh.transform(mat) result = bmesh.ops.convex_hull(mesh, input=mesh.verts, use_existing_faces=False) BMVert = bmesh.types.BMVert verts = itertools.takewhile(lambda x: isinstance(x, BMVert), result["geom"]) physical.verts = [hsVector3(*i.co) for i in verts] def _export_sphere(self, bo, physical, local_space, mat): """Exports sphere bounds based on the object""" physical.boundsType = plSimDefs.kSphereBounds vertices = self._convert_mesh_data(bo, physical, local_space, mat, indices=False) physical.calcSphereBounds(vertices) def _export_trimesh(self, bo, physical, local_space, mat): """Exports an object's mesh as exact physical bounds""" # Triangle meshes MAY optionally specify a proxy object to fetch the triangles from... mod = bo.plasma_modifiers.collision if mod.enabled and mod.proxy_object is not None: physical.boundsType = plSimDefs.kProxyBounds vertices, indices = self._convert_mesh_data(mod.proxy_object, physical, local_space, mat) else: physical.boundsType = plSimDefs.kExplicitBounds vertices, indices = self._convert_mesh_data(bo, physical, local_space, mat) 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 @property def _report(self): return self._exporter().report