|
|
|
# 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 bpy
|
|
|
|
import mathutils
|
|
|
|
from PyHSPlasma import *
|
|
|
|
import weakref
|
|
|
|
|
|
|
|
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)
|
|
|
|
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, indices=True, mesh_func=None):
|
|
|
|
try:
|
|
|
|
mesh = bo.to_mesh(bpy.context.scene, True, "RENDER", calc_tessface=False)
|
|
|
|
except:
|
|
|
|
raise ExportError("Unable to convert object [{}] '{}' to a mesh. Make sure you didn't try to use this object as a region!",
|
|
|
|
bo.type, bo.name)
|
|
|
|
mat = bo.matrix_world
|
|
|
|
|
|
|
|
with TemporaryObject(mesh, bpy.data.meshes.remove):
|
|
|
|
if mesh_func is not None:
|
|
|
|
mesh_func(mesh)
|
|
|
|
|
|
|
|
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
|
|
|
|
mesh.transform(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, 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):
|
|
|
|
# No mass and no emedded xform, so we force worldspace collision.
|
|
|
|
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) 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
|
|
|
|
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
|
|
|
|
|
|
|
|
# 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
|
|
|
|
|
|
|
|
if ver <= pvPots:
|
|
|
|
local_space = physical.mass > 0.0
|
|
|
|
else:
|
|
|
|
local_space = self._exporter().has_coordiface(bo)
|
|
|
|
self._bounds_converters[bounds](bo, physical, local_space)
|
|
|
|
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):
|
|
|
|
"""Exports box bounds based on the object"""
|
|
|
|
physical.boundsType = plSimDefs.kBoxBounds
|
|
|
|
|
|
|
|
vertices = self._convert_mesh_data(bo, physical, local_space, indices=False)
|
|
|
|
physical.calcBoxBounds(vertices)
|
|
|
|
|
|
|
|
def _export_hull(self, bo, physical, local_space):
|
|
|
|
"""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...
|
|
|
|
def _bake_hull(mesh):
|
|
|
|
# The bmesh API for doing this is trash, so we will link this temporary mesh to an
|
|
|
|
# even more temporary object so we can use the traditional operator.
|
|
|
|
# Unless you want to write some code to do this by hand...??? (not me)
|
|
|
|
bo = bpy.data.objects.new("BMeshSucks", mesh)
|
|
|
|
bpy.context.scene.objects.link(bo)
|
|
|
|
with TemporaryObject(bo, bpy.data.objects.remove):
|
|
|
|
bpy.context.scene.objects.active = bo
|
|
|
|
bpy.ops.object.mode_set(mode="EDIT")
|
|
|
|
bpy.ops.mesh.select_all(action="SELECT")
|
|
|
|
bpy.ops.mesh.convex_hull(use_existing_faces=False)
|
|
|
|
bpy.ops.object.mode_set(mode="OBJECT")
|
|
|
|
|
|
|
|
physical.verts = self._convert_mesh_data(bo, physical, local_space, indices=False,
|
|
|
|
mesh_func=_bake_hull)
|
|
|
|
|
|
|
|
def _export_sphere(self, bo, physical, local_space):
|
|
|
|
"""Exports sphere bounds based on the object"""
|
|
|
|
physical.boundsType = plSimDefs.kSphereBounds
|
|
|
|
|
|
|
|
vertices = self._convert_mesh_data(bo, physical, local_space, indices=False)
|
|
|
|
physical.calcSphereBounds(vertices)
|
|
|
|
|
|
|
|
def _export_trimesh(self, bo, physical, local_space):
|
|
|
|
"""Exports an object's mesh as exact physical bounds"""
|
|
|
|
physical.boundsType = plSimDefs.kExplicitBounds
|
|
|
|
|
|
|
|
vertices, indices = self._convert_mesh_data(bo, physical, local_space)
|
|
|
|
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
|