You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 

356 lines
17 KiB

# 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 <http://www.gnu.org/licenses/>.
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 mod.dynamic_blocker and not mod.avatar_blocker:
physical.collideGroup = (1 << plSimDefs.kGroupDynamic)
physical.memberGroup = plSimDefs.kGroupLOSOnly
elif not mod.avatar_blocker and not mod.dynamic_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(f"{bo.name}: Physical memberGroup overwritten!")
physical.memberGroup = member_group
# Sanity checking: only TPotS/Havok fully supports triangle mesh detector regions.
# ODE and PhysX 4.1 outright do not support them, and PhysX 2.6 only offers partial support,
# so warn or explode as appropriate. Note that we test against the physical itself in case
# shenanigans were performed by some of the exporters.
if physical.memberGroup == plSimDefs.kGroupDetector and physical.boundsType in (plSimDefs.kExplicitBounds, plSimDefs.kProxyBounds):
msg = f"'{bo.name}': Triangle mesh regions are poorly supported. Use a convex hull or box instead."
if ver <= pvPots:
self._report.port(msg)
else:
raise ExportError(msg)
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:
# Don't export flat planes as convex hulls - force them to triangle meshes.
volume = mesh.calc_volume()
if volume < 0.001:
self._report.warn(
"{}: Physical wants to be a convex hull but appears to be flat (volume={}), forcing to triangle mesh...",
bo.name, volume
)
self._export_trimesh(bo, physical, local_space, mat)
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