Browse Source

Merge tag 'v0.11a' into ci2

pull/272/head
Adam Johnson 3 years ago
parent
commit
4e8593597e
Signed by: Hoikas
GPG Key ID: 0B6515D6FF6F271E
  1. 607
      korman/exporter/animation.py
  2. 3
      korman/exporter/camera.py
  3. 31
      korman/exporter/convert.py
  4. 7
      korman/exporter/etlight.py
  5. 20
      korman/exporter/manager.py
  6. 148
      korman/exporter/material.py
  7. 231
      korman/exporter/mesh.py
  8. 3
      korman/exporter/outfile.py
  9. 64
      korman/exporter/physics.py
  10. 17
      korman/exporter/utils.py
  11. 3
      korman/idprops.py
  12. 15
      korman/nodes/node_core.py
  13. 52
      korman/nodes/node_python.py
  14. 14
      korman/nodes/node_responder.py
  15. 5
      korman/operators/op_mesh.py
  16. 68
      korman/operators/op_toolbox.py
  17. 4
      korman/properties/modifiers/__init__.py
  18. 65
      korman/properties/modifiers/anim.py
  19. 20
      korman/properties/modifiers/base.py
  20. 4
      korman/properties/modifiers/gui.py
  21. 6
      korman/properties/modifiers/physics.py
  22. 94
      korman/properties/modifiers/render.py
  23. 10
      korman/properties/modifiers/sound.py
  24. 2
      korman/properties/modifiers/water.py
  25. 10
      korman/properties/prop_object.py
  26. 17
      korman/properties/prop_world.py
  27. 6
      korman/ui/modifiers/anim.py
  28. 5
      korman/ui/modifiers/physics.py
  29. 30
      korman/ui/modifiers/render.py
  30. 17
      korman/ui/ui_menus.py
  31. 13
      korman/ui/ui_toolbox.py
  32. 23
      korman/ui/ui_world.py

607
korman/exporter/animation.py

@ -14,10 +14,13 @@
# along with Korman. If not, see <http://www.gnu.org/licenses/>.
import bpy
from collections import defaultdict
import functools
import itertools
import math
import mathutils
from PyHSPlasma import *
from typing import *
import weakref
from . import utils
@ -27,10 +30,10 @@ class AnimationConverter:
self._exporter = weakref.ref(exporter)
self._bl_fps = bpy.context.scene.render.fps
def _convert_frame_time(self, frame_num):
def _convert_frame_time(self, frame_num : int) -> float:
return frame_num / self._bl_fps
def convert_object_animations(self, bo, so):
def convert_object_animations(self, bo, so) -> None:
if not bo.plasma_object.has_animation_data:
return
@ -72,7 +75,8 @@ class AnimationConverter:
# There is a race condition in the client with animation loading. It expects for modifiers
# to be listed on the SceneObject in a specific order. D'OH! So, always use these funcs.
agmod, agmaster = self.get_anigraph_objects(bo, so)
atcanim = self._mgr.find_create_object(plATCAnim, so=so)
anim_mod = bo.plasma_modifiers.animation
atcanim = self._mgr.find_create_object(anim_mod.anim_type, so=so)
# Add the animation data to the ATC
for i in applicators:
@ -89,21 +93,23 @@ class AnimationConverter:
if i is not None:
yield i.frame_range[index]
atcanim.name = "(Entire Animation)"
sdl_name = anim_mod.obj_sdl_anim
atcanim.start = self._convert_frame_time(min(get_ranges(obj_action, data_action, index=0)))
atcanim.end = self._convert_frame_time(max(get_ranges(obj_action, data_action, index=1)))
# Marker points
if obj_action is not None:
for marker in obj_action.pose_markers:
atcanim.setMarker(marker.name, self._convert_frame_time(marker.frame))
# Fixme? Not sure if we really need to expose this...
atcanim.easeInMin = 1.0
atcanim.easeInMax = 1.0
atcanim.easeInLength = 1.0
atcanim.easeOutMin = 1.0
atcanim.easeOutMax = 1.0
atcanim.easeOutLength = 1.0
if isinstance(atcanim, plAgeGlobalAnim):
atcanim.globalVarName = anim_mod.obj_sdl_anim
if isinstance(atcanim, plATCAnim):
# Marker points
if obj_action is not None:
for marker in obj_action.pose_markers:
atcanim.setMarker(marker.name, self._convert_frame_time(marker.frame))
# Fixme? Not sure if we really need to expose this...
atcanim.easeInMin = 1.0
atcanim.easeInMax = 1.0
atcanim.easeInLength = 1.0
atcanim.easeOutMin = 1.0
atcanim.easeOutMax = 1.0
atcanim.easeOutLength = 1.0
def _convert_camera_animation(self, bo, so, obj_fcurves, data_fcurves):
if data_fcurves:
@ -188,7 +194,7 @@ class AnimationConverter:
return None
energy_curve = next((i for i in fcurves if i.data_path == "energy" and i.keyframe_points), None)
color_curves = sorted((i for i in fcurves if i.data_path == "color" and i.keyframe_points), key=lambda x: x.array_index)
color_curves = [i for i in fcurves if i.data_path == "color" and i.keyframe_points]
if energy_curve is None and color_curves is None:
return None
elif lamp.use_only_shadow:
@ -199,10 +205,15 @@ class AnimationConverter:
return None
# OK Specular is easy. We just toss out the color as a point3.
color_keyframes, color_bez = self._process_keyframes(color_curves, convert=lambda x: x * -1.0 if lamp.use_negative else None)
def convert_specular_animation(color):
if lamp.use_negative:
return map(lambda x: x * -1.0, color)
else:
return color
color_keyframes, color_bez = self._process_keyframes(color_curves, 3, lamp.color, convert_specular_animation)
if color_keyframes and lamp.use_specular:
channel = plPointControllerChannel()
channel.controller = self._make_point3_controller(color_curves, color_keyframes, color_bez, lamp.color)
channel.controller = self._make_point3_controller(color_keyframes, color_bez)
applicator = plLightSpecularApplicator()
applicator.channelName = name
applicator.channel = channel
@ -211,18 +222,20 @@ class AnimationConverter:
# Hey, look, it's a third way to process FCurves. YAY!
def convert_diffuse_animation(color, energy):
if lamp.use_negative:
return { key: (0.0 - value) * energy[0] for key, value in color.items() }
proc = lambda x: x * -1.0 * energy[0]
else:
return { key: value * energy[0] for key, value in color.items() }
diffuse_defaults = { "color": lamp.color, "energy": lamp.energy }
proc = lambda x: x * energy[0]
return map(proc, color)
diffuse_channels = dict(color=3, energy=1)
diffuse_defaults = dict(color=lamp.color, energy=lamp.energy)
diffuse_fcurves = color_curves + [energy_curve,]
diffuse_keyframes = self._process_fcurves(diffuse_fcurves, convert_diffuse_animation, diffuse_defaults)
diffuse_keyframes = self._process_fcurves(diffuse_fcurves, diffuse_channels, 3, convert_diffuse_animation, diffuse_defaults)
if not diffuse_keyframes:
return None
# Whew.
channel = plPointControllerChannel()
channel.controller = self._make_point3_controller([], diffuse_keyframes, False, [])
channel.controller = self._make_point3_controller(diffuse_keyframes, False)
applicator = plLightDiffuseApplicator()
applicator.channelName = name
applicator.channel = channel
@ -236,8 +249,16 @@ class AnimationConverter:
distance_fcurve = next((i for i in fcurves if i.data_path == "distance"), None)
if energy_fcurve is None and distance_fcurve is None:
return None
light_converter = self._exporter().light
intensity, atten_end = light_converter.convert_attenuation(lamp)
light_converter, report = self._exporter().light, self._exporter().report
omni_fcurves = [distance_fcurve, energy_fcurve]
omni_channels = dict(distance=1, energy=1)
omni_defaults = dict(distance=lamp.distance, energy=lamp.energy)
def convert_omni_atten(distance, energy):
intens = abs(energy[0])
atten_end = distance[0] if lamp.use_sphere else distance[0] * 2
return light_converter.convert_attenuation_linear(intens, atten_end)
# All types allow animating cutoff
if distance_fcurve is not None:
@ -252,15 +273,9 @@ class AnimationConverter:
falloff = lamp.falloff_type
if falloff == "CONSTANT":
if energy_fcurve is not None:
self._exporter().report.warn("Constant attenuation cannot be animated in Plasma", ident=3)
report.warn("Constant attenuation cannot be animated in Plasma", ident=3)
elif falloff == "INVERSE_LINEAR":
def convert_linear_atten(distance, energy):
intens = abs(energy[0])
atten_end = distance[0] if lamp.use_sphere else distance[0] * 2
return light_converter.convert_attenuation_linear(intens, atten_end)
keyframes = self._process_fcurves([distance_fcurve, energy_fcurve], convert_linear_atten,
{"distance": lamp.distance, "energy": lamp.energy})
keyframes = self._process_fcurves(omni_fcurves, omni_channels, 1, convert_omni_atten, omni_defaults)
if keyframes:
channel = plScalarControllerChannel()
channel.controller = self._make_scalar_leaf_controller(keyframes, False)
@ -270,13 +285,8 @@ class AnimationConverter:
yield applicator
elif falloff == "INVERSE_SQUARE":
if self._mgr.getVer() >= pvMoul:
def convert_quadratic_atten(distance, energy):
intens = abs(energy[0])
atten_end = distance[0] if lamp.use_sphere else distance[0] * 2
return light_converter.convert_attenuation_quadratic(intens, atten_end)
keyframes = self._process_fcurves([distance_fcurve, energy_fcurve], convert_quadratic_atten,
{"distance": lamp.distance, "energy": lamp.energy})
report.port("Lamp {} Falloff animations are only supported in Myst Online: Uru Live", falloff, indent=3)
keyframes = self._process_fcurves(omni_fcurves, omni_channels, 1, convert_omni_atten, omni_defaults)
if keyframes:
channel = plScalarControllerChannel()
channel.controller = self._make_scalar_leaf_controller(keyframes, False)
@ -285,19 +295,15 @@ class AnimationConverter:
applicator.channel = channel
yield applicator
else:
self._exporter().report.port("Lamp Falloff '{}' animations only partially supported for this version of Plasma", falloff, indent=3)
report.warn("Lamp {} Falloff animations are not supported for this version of Plasma", falloff, indent=3)
else:
self._exporter().report.warn("Lamp Falloff '{}' animations are not supported".format(falloff), ident=3)
report.warn("Lamp Falloff '{}' animations are not supported", falloff, ident=3)
def _convert_sound_volume_animation(self, name, fcurves, soundemit):
if not fcurves:
return None
def convert_volume(value):
if value == 0.0:
return 0.0
else:
return math.log10(value) * 20.0
convert_volume = lambda x: math.log10(max(.01, x / 100.0)) * 20.0
for sound in soundemit.sounds:
path = "{}.volume".format(sound.path_from_id())
@ -341,8 +347,11 @@ class AnimationConverter:
size = spot_size[0]
value = size - (blend * size)
return math.degrees(value)
defaults = { "spot_blend": lamp.spot_blend, "spot_size": lamp.spot_size }
keyframes = self._process_fcurves([blend_fcurve, size_fcurve], convert_spot_inner, defaults)
inner_fcurves = [blend_fcurve, size_fcurve]
inner_channels = dict(spot_blend=1, spot_size=1)
inner_defaults = dict(spot_blend=lamp.spot_blend, spot_size=lamp.spot_size)
keyframes = self._process_fcurves(inner_fcurves, inner_channels, 1, convert_spot_inner, inner_defaults)
if keyframes:
channel = plScalarControllerChannel()
@ -352,7 +361,7 @@ class AnimationConverter:
applicator.channel = channel
yield applicator
def _convert_transform_animation(self, name, fcurves, xform, allow_empty=False):
def _convert_transform_animation(self, name, fcurves, xform, allow_empty=False) -> Union[None, plMatrixChannelApplicator]:
tm = self.convert_transform_controller(fcurves, xform, allow_empty)
if tm is None and not allow_empty:
return None
@ -367,13 +376,14 @@ class AnimationConverter:
return applicator
def convert_transform_controller(self, fcurves, xform, allow_empty=False):
def convert_transform_controller(self, fcurves, xform, allow_empty=False) -> Union[None, plCompoundController]:
if not fcurves and not allow_empty:
return None
pos = self.make_pos_controller(fcurves, xform)
rot = self.make_rot_controller(fcurves, xform)
scale = self.make_scale_controller(fcurves, xform)
pos = self.make_pos_controller(fcurves, "location", xform.to_translation())
# TODO: support rotation_quaternion
rot = self.make_rot_controller(fcurves, "rotation_euler", xform.to_euler())
scale = self.make_scale_controller(fcurves, "scale", xform.to_scale())
if pos is None and rot is None and scale is None:
if not allow_empty:
return None
@ -384,17 +394,17 @@ class AnimationConverter:
tm.Z = scale
return tm
def get_anigraph_keys(self, bo=None, so=None):
def get_anigraph_keys(self, bo=None, so=None) -> Tuple[plKey, plKey]:
mod = self._mgr.find_create_key(plAGModifier, so=so, bl=bo)
master = self._mgr.find_create_key(plAGMasterMod, so=so, bl=bo)
return mod, master
def get_anigraph_objects(self, bo=None, so=None):
def get_anigraph_objects(self, bo=None, so=None) -> Tuple[plAGModifier, plAGMasterMod]:
mod = self._mgr.find_create_object(plAGModifier, so=so, bl=bo)
master = self._mgr.find_create_object(plAGMasterMod, so=so, bl=bo)
return mod, master
def get_animation_key(self, bo, so=None):
def get_animation_key(self, bo, so=None) -> plKey:
# we might be controlling more than one animation. isn't that cute?
# https://www.youtube.com/watch?v=hspNaoxzNbs
# (but obviously this is not wrong...)
@ -404,72 +414,65 @@ class AnimationConverter:
else:
return self.get_anigraph_keys(bo, so)[1]
def make_matrix44_controller(self, fcurves, pos_path, scale_path, pos_default, scale_default):
def convert_matrix_keyframe(**kwargs):
pos = kwargs.get(pos_path)
scale = kwargs.get(scale_path)
# Since only some position curves may be supplied, construct dict with all positions
allpos = dict(enumerate(pos_default))
allscale = dict(enumerate(scale_default))
allpos.update(pos)
allscale.update(scale)
def make_matrix44_controller(self, fcurves, pos_path : str, scale_path : str, pos_default, scale_default) -> Union[None, plLeafController]:
def convert_matrix_keyframe(**kwargs) -> hsMatrix44:
pos = kwargs[pos_path]
scale = kwargs[scale_path]
matrix = hsMatrix44()
# Note: scale and pos are dicts, so we can't unpack
matrix.setTranslate(hsVector3(allpos[0], allpos[1], allpos[2]))
matrix.setScale(hsVector3(allscale[0], allscale[1], allscale[2]))
matrix.setTranslate(hsVector3(*pos))
matrix.setScale(hsVector3(*scale))
return matrix
fcurves = [i for i in fcurves if i.data_path == pos_path or i.data_path == scale_path]
if not fcurves:
return None
channels = { pos_path: 3, scale_path: 3 }
default_values = { pos_path: pos_default, scale_path: scale_default }
keyframes = self._process_fcurves(fcurves, convert_matrix_keyframe, default_values)
keyframes = self._process_fcurves(fcurves, channels, 1, convert_matrix_keyframe, default_values)
if not keyframes:
return None
# Now we make the controller
return self._make_matrix44_controller(keyframes)
def make_pos_controller(self, fcurves, default_xform, convert=None):
pos_curves = [i for i in fcurves if i.data_path == "location" and i.keyframe_points]
keyframes, bez_chans = self._process_keyframes(pos_curves, convert)
def make_pos_controller(self, fcurves, data_path : str, default_xform, convert=None) -> Union[None, plLeafController]:
pos_curves = [i for i in fcurves if i.data_path == data_path and i.keyframe_points]
keyframes, bez_chans = self._process_keyframes(pos_curves, 3, default_xform, convert)
if not keyframes:
return None
# At one point, I had some... insanity here to try to crush bezier channels and hand off to
# blah blah blah... As it turns out, point3 keyframe's tangents are vector3s :)
ctrl = self._make_point3_controller(pos_curves, keyframes, bez_chans, default_xform.to_translation())
ctrl = self._make_point3_controller(keyframes, bez_chans)
return ctrl
def make_rot_controller(self, fcurves, default_xform, convert=None):
# TODO: support rotation_quaternion
rot_curves = [i for i in fcurves if i.data_path == "rotation_euler" and i.keyframe_points]
keyframes, bez_chans = self._process_keyframes(rot_curves, convert=None)
def make_rot_controller(self, fcurves, data_path : str, default_xform, convert=None) -> Union[None, plCompoundController, plLeafController]:
rot_curves = [i for i in fcurves if i.data_path == data_path and i.keyframe_points]
keyframes, bez_chans = self._process_keyframes(rot_curves, 3, default_xform, convert=None)
if not keyframes:
return None
# Ugh. Unfortunately, it appears Blender's default interpolation is bezier. So who knows if
# many users will actually see the benefit here? Makes me sad.
if bez_chans:
ctrl = self._make_scalar_compound_controller(rot_curves, keyframes, bez_chans, default_xform.to_euler())
ctrl = self._make_scalar_compound_controller(keyframes, bez_chans)
else:
ctrl = self._make_quat_controller(rot_curves, keyframes, default_xform.to_euler())
ctrl = self._make_quat_controller( keyframes)
return ctrl
def make_scale_controller(self, fcurves, default_xform, convert=None):
scale_curves = [i for i in fcurves if i.data_path == "scale" and i.keyframe_points]
keyframes, bez_chans = self._process_keyframes(scale_curves, convert)
def make_scale_controller(self, fcurves, data_path : str, default_xform, convert=None) -> plLeafController:
scale_curves = [i for i in fcurves if i.data_path == data_path and i.keyframe_points]
keyframes, bez_chans = self._process_keyframes(scale_curves, 3, default_xform, convert)
if not keyframes:
return None
# There is no such thing as a compound scale controller... in Plasma, anyway.
ctrl = self._make_scale_value_controller(scale_curves, keyframes, bez_chans, default_xform)
ctrl = self._make_scale_value_controller(keyframes, bez_chans)
return ctrl
def make_scalar_leaf_controller(self, fcurve, convert=None):
def make_scalar_leaf_controller(self, fcurve, convert=None) -> Union[None, plLeafController]:
keyframes, bezier = self._process_fcurve(fcurve, convert)
if not keyframes:
return None
@ -477,7 +480,7 @@ class AnimationConverter:
ctrl = self._make_scalar_leaf_controller(keyframes, bezier)
return ctrl
def _make_matrix44_controller(self, keyframes):
def _make_matrix44_controller(self, keyframes) -> plLeafController:
ctrl = plLeafController()
keyframe_type = hsKeyFrame.kMatrix44KeyFrame
exported_frames = []
@ -487,52 +490,32 @@ class AnimationConverter:
exported.frame = keyframe.frame_num
exported.frameTime = keyframe.frame_time
exported.type = keyframe_type
exported.value = keyframe.value
exported.value = keyframe.values[0]
exported_frames.append(exported)
ctrl.keys = (exported_frames, keyframe_type)
return ctrl
def _make_point3_controller(self, fcurves, keyframes, bezier, default_xform):
def _make_point3_controller(self, keyframes, bezier) -> plLeafController:
ctrl = plLeafController()
subctrls = ("X", "Y", "Z")
keyframe_type = hsKeyFrame.kBezPoint3KeyFrame if bezier else hsKeyFrame.kPoint3KeyFrame
exported_frames = []
ctrl_fcurves = { i.array_index: i for i in fcurves }
for keyframe in keyframes:
exported = hsPoint3Key()
exported.frame = keyframe.frame_num
exported.frameTime = keyframe.frame_time
exported.type = keyframe_type
in_tan = hsVector3()
out_tan = hsVector3()
value = hsVector3()
for i, subctrl in enumerate(subctrls):
fval = keyframe.values.get(i, None)
if fval is not None:
setattr(value, subctrl, fval)
setattr(in_tan, subctrl, keyframe.in_tans[i])
setattr(out_tan, subctrl, keyframe.out_tans[i])
else:
try:
setattr(value, subctrl, ctrl_fcurves[i].evaluate(keyframe.frame_num_blender))
except KeyError:
setattr(value, subctrl, default_xform[i])
setattr(in_tan, subctrl, 0.0)
setattr(out_tan, subctrl, 0.0)
exported.inTan = in_tan
exported.outTan = out_tan
exported.value = value
exported.inTan = hsVector3(*keyframe.in_tans)
exported.outTan = hsVector3(*keyframe.out_tans)
exported.value = hsVector3(*keyframe.values)
exported_frames.append(exported)
ctrl.keys = (exported_frames, keyframe_type)
return ctrl
def _make_quat_controller(self, fcurves, keyframes, default_xform):
def _make_quat_controller(self, keyframes) -> plLeafController:
ctrl = plLeafController()
keyframe_type = hsKeyFrame.kQuatKeyFrame
exported_frames = []
ctrl_fcurves = { i.array_index: i for i in fcurves }
for keyframe in keyframes:
exported = hsQuatKey()
@ -541,58 +524,36 @@ class AnimationConverter:
exported.type = keyframe_type
# NOTE: quat keyframes don't do bezier nonsense
value = mathutils.Euler()
for i in range(3):
fval = keyframe.values.get(i, None)
if fval is not None:
value[i] = fval
else:
try:
value[i] = ctrl_fcurves[i].evaluate(keyframe.frame_num_blender)
except KeyError:
value[i] = default_xform[i]
quat = value.to_quaternion()
exported.value = utils.quaternion(quat)
value = mathutils.Euler(keyframe.values)
exported.value = utils.quaternion(value.to_quaternion())
exported_frames.append(exported)
ctrl.keys = (exported_frames, keyframe_type)
return ctrl
def _make_scalar_compound_controller(self, fcurves, keyframes, bez_chans, default_xform):
def _make_scalar_compound_controller(self, keyframes, bez_chans) -> plCompoundController:
ctrl = plCompoundController()
subctrls = ("X", "Y", "Z")
for i in subctrls:
setattr(ctrl, i, plLeafController())
exported_frames = ([], [], [])
ctrl_fcurves = { i.array_index: i for i in fcurves }
for keyframe in keyframes:
for i, subctrl in enumerate(subctrls):
fval = keyframe.values.get(i, None)
if fval is not None:
keyframe_type = hsKeyFrame.kBezScalarKeyFrame if i in bez_chans else hsKeyFrame.kScalarKeyFrame
exported = hsScalarKey()
exported.frame = keyframe.frame_num
exported.frameTime = keyframe.frame_time
exported.inTan = keyframe.in_tans[i]
exported.outTan = keyframe.out_tans[i]
exported.type = keyframe_type
exported.value = fval
exported_frames[i].append(exported)
keyframe_type = hsKeyFrame.kBezScalarKeyFrame if i in bez_chans else hsKeyFrame.kScalarKeyFrame
exported = hsScalarKey()
exported.frame = keyframe.frame_num
exported.frameTime = keyframe.frame_time
exported.inTan = keyframe.in_tans[i]
exported.outTan = keyframe.out_tans[i]
exported.type = keyframe_type
exported.value = keyframe.values[i]
exported_frames[i].append(exported)
for i, subctrl in enumerate(subctrls):
my_keyframes = exported_frames[i]
# ensure this controller has at least ONE keyframe
if not my_keyframes:
hack_frame = hsScalarKey()
hack_frame.frame = 0
hack_frame.frameTime = 0.0
hack_frame.type = hsKeyFrame.kScalarKeyFrame
hack_frame.value = default_xform[i]
my_keyframes.append(hack_frame)
getattr(ctrl, subctrl).keys = (my_keyframes, my_keyframes[0].type)
return ctrl
def _make_scalar_leaf_controller(self, keyframes, bezier):
def _make_scalar_leaf_controller(self, keyframes, bezier) -> plLeafController:
ctrl = plLeafController()
keyframe_type = hsKeyFrame.kBezScalarKeyFrame if bezier else hsKeyFrame.kScalarKeyFrame
exported_frames = []
@ -601,239 +562,195 @@ class AnimationConverter:
exported = hsScalarKey()
exported.frame = keyframe.frame_num
exported.frameTime = keyframe.frame_time
exported.inTan = keyframe.in_tan
exported.outTan = keyframe.out_tan
exported.inTan = keyframe.in_tans[0]
exported.outTan = keyframe.out_tans[0]
exported.type = keyframe_type
exported.value = keyframe.value
exported.value = keyframe.values[0]
exported_frames.append(exported)
ctrl.keys = (exported_frames, keyframe_type)
return ctrl
def _make_scale_value_controller(self, fcurves, keyframes, bez_chans, default_xform):
subctrls = ("X", "Y", "Z")
def _make_scale_value_controller(self, keyframes, bez_chans) -> plLeafController:
keyframe_type = hsKeyFrame.kBezScaleKeyFrame if bez_chans else hsKeyFrame.kScaleKeyFrame
exported_frames = []
ctrl_fcurves = { i.array_index: i for i in fcurves }
default_scale = default_xform.to_scale()
unit_quat = default_xform.to_quaternion()
unit_quat.normalize()
unit_quat = utils.quaternion(unit_quat)
# Hmm... This smells... But it was basically doing this before the rewrite.
unit_quat = hsQuat(0.0, 0.0, 0.0, 1.0)
for keyframe in keyframes:
exported = hsScaleKey()
exported.frame = keyframe.frame_num
exported.frameTime = keyframe.frame_time
exported.type = keyframe_type
in_tan = hsVector3()
out_tan = hsVector3()
value = hsVector3()
for i, subctrl in enumerate(subctrls):
fval = keyframe.values.get(i, None)
if fval is not None:
setattr(value, subctrl, fval)
setattr(in_tan, subctrl, keyframe.in_tans[i])
setattr(out_tan, subctrl, keyframe.out_tans[i])
else:
try:
setattr(value, subctrl, ctrl_fcurves[i].evaluate(keyframe.frame_num_blender))
except KeyError:
setattr(value, subctrl, default_scale[i])
setattr(in_tan, subctrl, 0.0)
setattr(out_tan, subctrl, 0.0)
exported.inTan = in_tan
exported.outTan = out_tan
exported.value = (value, unit_quat)
exported.inTan = hsVector3(*keyframe.in_tans)
exported.outTan = hsVector3(*keyframe.out_tans)
exported.value = (hsVector3(*keyframe.values), unit_quat)
exported_frames.append(exported)
ctrl = plLeafController()
ctrl.keys = (exported_frames, keyframe_type)
return ctrl
def _process_fcurve(self, fcurve, convert=None):
def _sort_and_dedupe_keyframes(self, keyframes : Dict) -> Sequence:
"""Takes in the final, unsorted keyframe sequence and sorts it. If all keyframes are
equivalent, eg due to a convert function, then they are discarded."""
num_keyframes = len(keyframes)
keyframes_sorted = [keyframes[i] for i in sorted(keyframes)]
# If any keyframe's value is equivalent to its boundary keyframes, discard it.
def filter_boundaries(i):
if i == 0 or i == num_keyframes - 1:
return False
left, me, right = keyframes_sorted[i - 1], keyframes_sorted[i], keyframes_sorted[i + 1]
return left.values == me.values == right.values
filtered_indices = list(itertools.filterfalse(filter_boundaries, range(num_keyframes)))
if len(filtered_indices) == 2:
if keyframes_sorted[filtered_indices[0]].values == keyframes_sorted[filtered_indices[1]].values:
return []
return [keyframes_sorted[i] for i in filtered_indices]
def _process_fcurve(self, fcurve, convert=None) -> Tuple[Sequence, AbstractSet]:
"""Like _process_keyframes, but for one fcurve"""
# Adapt from incoming single item sequence to a single argument.
if convert is not None:
single_convert = lambda x: convert(x[0])
else:
single_convert = None
# Can't proxy to _process_fcurves because it only supports linear interoplation.
return self._process_keyframes([fcurve], 1, [0.0], single_convert)
def _santize_converted_values(self, num_channels : int, raw_values : Union[Dict, Sequence], convert : Callable):
assert convert is not None
if isinstance(raw_values, Dict):
values = convert(**raw_values)
elif isinstance(raw_values, Sequence):
values = convert(raw_values)
else:
raise AssertionError("Unexpected type for raw_values: {}".format(raw_values.__class__))
if not isinstance(values, Sequence) and isinstance(values, Iterable):
values = tuple(values)
if not isinstance(values, Sequence):
assert num_channels == 1, "Converter returned 1 value but expected {}".format(num_channels)
values = (values,)
else:
assert len(values) == num_channels, "Converter returned {} values but expected {}".format(len(values), num_channels)
return values
def _process_fcurves(self, fcurves : Sequence, channels : Dict[str, int], result_channels : int,
convert : Callable, defaults : Dict[str, Union[float, Sequence]]) -> Sequence:
"""This consumes a sequence of Blender FCurves that map to a single Plasma controller.
Like `_process_keyframes()`, except the converter function is mandatory, and each
Blender `data_path` must have a fixed number of channels.
"""
# TODO: This fxn should probably issue a warning if any keyframes use bezier interpolation.
# But there's no indication given by any other fxn when an invalid interpolation mode is
# given, so what can you do?
keyframe_data = type("KeyFrameData", (), {})
fps = self._bl_fps
pi = math.pi
fps, pi = self._bl_fps, math.pi
keyframes = {}
bezier = False
fcurve.update()
for fkey in fcurve.keyframe_points:
keyframe = keyframe_data()
frame_num, value = fkey.co
if fps == 30.0:
keyframe.frame_num = int(frame_num)
else:
keyframe.frame_num = int(frame_num * (30.0 / fps))
keyframe.frame_time = frame_num / fps
if fkey.interpolation == "BEZIER":
keyframe.in_tan = -(value - fkey.handle_left[1]) / (frame_num - fkey.handle_left[0]) / fps / (2 * pi)
keyframe.out_tan = (value - fkey.handle_right[1]) / (frame_num - fkey.handle_right[0]) / fps / (2 * pi)
bezier = True
else:
keyframe.in_tan = 0.0
keyframe.out_tan = 0.0
keyframe.value = value if convert is None else convert(value)
keyframes[frame_num] = keyframe
final_keyframes = [keyframes[i] for i in sorted(keyframes)]
return (final_keyframes, bezier)
def _process_fcurves(self, fcurves, convert, defaults=None):
"""Processes FCurves of different data sets and converts them into a single list of keyframes.
This should be used when multiple Blender fields map to a single Plasma option."""
class KeyFrameData:
def __init__(self):
self.values = {}
fps = self._bl_fps
pi = math.pi
# It is assumed therefore that any multichannel FCurves will have all channels represented.
# This seems fairly safe with my experiments with Lamp colors...
grouped_fcurves = {}
for fcurve in fcurves:
if fcurve is None:
continue
grouped_fcurves = defaultdict(dict)
for fcurve in (i for i in fcurves if i is not None):
fcurve.update()
if fcurve.data_path in grouped_fcurves:
grouped_fcurves[fcurve.data_path][fcurve.array_index] = fcurve
else:
grouped_fcurves[fcurve.data_path] = { fcurve.array_index: fcurve }
grouped_fcurves[fcurve.data_path][fcurve.array_index] = fcurve
# Default values for channels that are not animated
for key, value in defaults.items():
if key not in grouped_fcurves:
if hasattr(value, "__len__"):
grouped_fcurves[key] = value
fcurve_keyframes = defaultdict(functools.partial(defaultdict, dict))
for fcurve in (i for i in fcurves if i is not None):
for fkey in fcurve.keyframe_points:
fcurve_keyframes[fkey.co[0]][fcurve.data_path][fcurve.array_index] = fkey
def iter_channel_values(frame_num : int, fcurves : Dict, fkeys : Dict, num_channels : int, defaults : Union[float, Sequence]):
for i in range(num_channels):
fkey = fkeys.get(i, None)
if fkey is None:
fcurve = fcurves.get(i, None)
if fcurve is None:
# We would like to test this to see if it makes sense, but Blender's mathutils
# types don't actually implement the sequence protocol. So, we'll have to
# just try to subscript it and see what happens.
try:
yield defaults[i]
except:
assert num_channels == 1, "Got a non-subscriptable default for a multi-channel keyframe."
yield defaults
else:
yield fcurve.evaluate(frame_num)
else:
grouped_fcurves[key] = [value,]
yield fkey.co[1]
# Assemble a dict { PlasmaFrameNum: { FCurveDataPath: KeyFrame } }
keyframe_points = {}
for fcurve in fcurves:
if fcurve is None:
continue
for keyframe in fcurve.keyframe_points:
frame_num_blender, value = keyframe.co
frame_num = int(frame_num_blender * (30.0 / fps))
# This is a temporary keyframe, so we're not going to worry about converting everything
# Only the frame number to Plasma so we can go ahead and merge any rounded dupes
entry, data = keyframe_points.get(frame_num), None
if entry is None:
entry = {}
keyframe_points[frame_num] = entry
else:
data = entry.get(fcurve.data_path)
if data is None:
data = KeyFrameData()
data.frame_num = frame_num
data.frame_num_blender = frame_num_blender
entry[fcurve.data_path] = data
data.values[fcurve.array_index] = value
# Now, we loop through our assembled keyframes and interpolate any missing data using the FCurves
fcurve_chans = { key: len(value) for key, value in grouped_fcurves.items() }
expected_values = sum(fcurve_chans.values())
all_chans = frozenset(grouped_fcurves.keys())
# We will also do the final convert here as well...
final_keyframes = []
for frame_num in sorted(keyframe_points.copy().keys()):
keyframes = keyframe_points[frame_num]
frame_num_blender = next(iter(keyframes.values())).frame_num_blender
# If any data_paths are missing, init a dummy
missing_channels = all_chans - frozenset(keyframes.keys())
for chan in missing_channels:
dummy = KeyFrameData()
dummy.frame_num = frame_num
dummy.frame_num_blender = frame_num_blender
keyframes[chan] = dummy
# Ensure all values are filled out.
num_values = sum(map(len, (i.values for i in keyframes.values())))
if num_values != expected_values:
for chan, sorted_fcurves in grouped_fcurves.items():
chan_keyframes = keyframes[chan]
chan_values = fcurve_chans[chan]
if len(chan_keyframes.values) == chan_values:
continue
for i in range(chan_values):
if i not in chan_keyframes.values:
try:
fcurve = grouped_fcurves[chan][i]
except:
chan_keyframes.values[i] = defaults[chan]
else:
if isinstance(fcurve, bpy.types.FCurve):
chan_keyframes.values[i] = fcurve.evaluate(chan_keyframes.frame_num_blender)
else:
# it's actually a default value!
chan_keyframes.values[i] = fcurve
# All values are calculated! Now we convert the disparate key data into a single keyframe.
kwargs = { data_path: keyframe.values for data_path, keyframe in keyframes.items() }
final_keyframe = KeyFrameData()
final_keyframe.frame_num = frame_num
final_keyframe.frame_num_blender = frame_num_blender
final_keyframe.frame_time = frame_num / fps
value = convert(**kwargs)
if hasattr(value, "__len__"):
final_keyframe.in_tans = [0.0] * len(value)
final_keyframe.out_tans = [0.0] * len(value)
final_keyframe.values = value
else:
final_keyframe.in_tan = 0.0
final_keyframe.out_tan = 0.0
final_keyframe.value = value
final_keyframes.append(final_keyframe)
return final_keyframes
keyframes = {}
for frame_num, fkeys in fcurve_keyframes.items():
keyframe = keyframe_data()
# hope you don't have a frame 29.9 and frame 30.0...
keyframe.frame_num = int(frame_num * (30.0 / fps))
keyframe.frame_num_blender = frame_num
keyframe.frame_time = frame_num / fps
keyframe.values_raw = { data_path: tuple(iter_channel_values(frame_num, grouped_fcurves[data_path], fkeys, num_channels, defaults[data_path]))
for data_path, num_channels in channels.items() }
keyframe.values = self._santize_converted_values(result_channels, keyframe.values_raw, convert)
# Very gnawty
keyframe.in_tans = [0.0] * result_channels
keyframe.out_tans = [0.0] * result_channels
keyframes[frame_num] = keyframe
return self._sort_and_dedupe_keyframes(keyframes)
def _process_keyframes(self, fcurves, convert=None):
def _process_keyframes(self, fcurves, num_channels : int, default_values : Sequence, convert=None) -> Tuple[Sequence, AbstractSet]:
"""Groups all FCurves for the same frame together"""
keyframe_data = type("KeyFrameData", (), {})
fps = self._bl_fps
pi = math.pi
fps, pi = self._bl_fps, math.pi
keyframes = {}
bez_chans = set()
for fcurve in fcurves:
keyframes, fcurve_keyframes = {}, defaultdict(dict)
indexed_fcurves = { fcurve.array_index: fcurve for fcurve in fcurves if fcurve is not None }
for i, fcurve in indexed_fcurves.items():
fcurve.update()
for fkey in fcurve.keyframe_points:
frame_num, value = fkey.co
keyframe = keyframes.get(frame_num, None)
if keyframe is None:
keyframe = keyframe_data()
if fps == 30.0:
# hope you don't have a frame 29.9 and frame 30.0...
keyframe.frame_num = int(frame_num)
else:
keyframe.frame_num = int(frame_num * (30.0 / fps))
keyframe.frame_num_blender = frame_num
keyframe.frame_time = frame_num / fps
keyframe.in_tans = {}
keyframe.out_tans = {}
keyframe.values = {}
keyframes[frame_num] = keyframe
idx = fcurve.array_index
keyframe.values[idx] = value if convert is None else convert(value)
# Calculate the bezier interpolation nonsense
if fkey.interpolation == "BEZIER":
keyframe.in_tans[idx] = -(value - fkey.handle_left[1]) / (frame_num - fkey.handle_left[0]) / fps / (2 * pi)
keyframe.out_tans[idx] = (value - fkey.handle_right[1]) / (frame_num - fkey.handle_right[0]) / fps / (2 * pi)
bez_chans.add(idx)
fcurve_keyframes[fkey.co[0]][i] = fkey
def iter_values(frame_num, fkeys) -> Generator[float, None, None]:
for i in range(num_channels):
fkey = fkeys.get(i, None)
if fkey is not None:
yield fkey.co[1]
else:
keyframe.in_tans[idx] = 0.0
keyframe.out_tans[idx] = 0.0
fcurve = indexed_fcurves.get(i, None)
if fcurve is not None:
yield fcurve.evaluate(frame_num)
else:
yield default_values[i]
# Does this really need to be a set?
bez_chans = set()
for frame_num, fkeys in fcurve_keyframes.items():
keyframe = keyframe_data()
# hope you don't have a frame 29.9 and frame 30.0...
keyframe.frame_num = int(frame_num * (30.0 / fps))
keyframe.frame_num_blender = frame_num
keyframe.frame_time = frame_num / fps
keyframe.in_tans = [0.0] * num_channels
keyframe.out_tans = [0.0] * num_channels
keyframe.values_raw = tuple(iter_values(frame_num, fkeys))
if convert is None:
keyframe.values = keyframe.values_raw
else:
keyframe.values = self._santize_converted_values(num_channels, keyframe.values_raw, convert)
for i, fkey in ((i, fkey) for i, fkey in fkeys.items() if fkey.interpolation == "BEZIER"):
value = keyframe.values_raw[i]
keyframe.in_tans[i] = -(value - fkey.handle_left[1]) / (frame_num - fkey.handle_left[0]) / fps / (2 * pi)
keyframe.out_tans[i] = (value - fkey.handle_right[1]) / (frame_num - fkey.handle_right[0]) / fps / (2 * pi)
bez_chans.add(i)
keyframes[frame_num] = keyframe
# Return the keyframes in a sequence sorted by frame number
final_keyframes = [keyframes[i] for i in sorted(keyframes)]
return (final_keyframes, bez_chans)
return (self._sort_and_dedupe_keyframes(keyframes), bez_chans)
@property
def _mgr(self):

3
korman/exporter/camera.py

@ -206,6 +206,9 @@ class CameraConverter:
f1, f2 = fcurve.evaluate(begin), fcurve.evaluate(end)
if abs(f1 - f2) > 0.001:
break
# to avoid single/duplicate keyframe client crash (per Hoikas)
if any((len(i.keys) == 1 for i in (pos_ctrl.X, pos_ctrl.Y, pos_ctrl.Z) if i is not None)):
raise ExportError("'{}': Rail Camera must have more than one keyframe", bo.name)
else:
# The animation is a loop
path.flags |= plAnimPath.kWrap

31
korman/exporter/convert.py

@ -60,6 +60,7 @@ class Exporter:
# Step 0.8: Init the progress mgr
self.mesh.add_progress_presteps(self.report)
self.report.progress_add_step("Collecting Objects")
self.report.progress_add_step("Verify Competence")
self.report.progress_add_step("Harvesting Actors")
if self._op.lighting_method != "skip":
etlight.LightBaker.add_progress_steps(self.report)
@ -81,6 +82,10 @@ class Exporter:
# us to export (both in the Age and Object Properties)... fun
self._collect_objects()
# Step 2.1: Run through all the objects we collected in Step 2 and make sure there
# is no ruddy funny business going on.
self._check_sanity()
# Step 2.5: Run through all the objects we collected in Step 2 and see if any relationships
# that the artist made requires something to have a CoordinateInterface
self._harvest_actors()
@ -169,6 +174,20 @@ class Exporter:
inc_progress()
error.raise_if_error()
def _check_sanity(self):
self.report.progress_advance()
self.report.progress_range = len(self._objects)
inc_progress = self.report.progress_increment
self.report.msg("\nEnsuring Age is sane...")
for bl_obj in self._objects:
for mod in bl_obj.plasma_modifiers.modifiers:
fn = getattr(mod, "sanity_check", None)
if fn is not None:
fn()
inc_progress()
self.report.msg("... Age is grinning and holding a spatula. Must be OK, then.")
def _export_age_info(self):
# Make life slightly easier...
age_info = bpy.context.scene.world.plasma_age
@ -268,10 +287,18 @@ class Exporter:
def _export_mesh_blobj(self, so, bo):
self.animation.convert_object_animations(bo, so)
if bo.data.materials:
self.mesh.export_object(bo)
self.mesh.export_object(bo, so)
else:
self.report.msg("No material(s) on the ObData, so no drawables", indent=1)
def _export_font_blobj(self, so, bo):
self.animation.convert_object_animations(bo, so)
with utils.temporary_mesh_object(bo) as meshObj:
if bo.data.materials:
self.mesh.export_object(meshObj, so)
else:
self.report.msg("No material(s) on the ObData, so no drawables", indent=1)
def _export_referenced_node_trees(self):
self.report.progress_advance()
self.report.progress_range = len(self.want_node_trees)
@ -407,7 +434,7 @@ class Exporter:
if not valid_path:
filepath = bpy.context.blend_data.filepath
if not filepath:
filepath = self.filepath
filepath = self._op.filepath
filepath = str(Path(filepath).with_suffix(".ktc"))
age.texcache_path = filepath
return filepath

7
korman/exporter/etlight.py

@ -375,12 +375,9 @@ class LightBaker(_MeshManager):
if not self._generate_lightgroup(bo, user_lg):
return False
# I have heard tale of some moar "No valid image to bake to" boogs if there is a really
# old copy of the autocolor layer on the mesh. Nuke it.
autocolor = vcols.get("autocolor")
if autocolor is not None:
vcols.remove(autocolor)
autocolor = vcols.new("autocolor")
if autocolor is None:
autocolor = vcols.new("autocolor")
toggle.track(vcols, "active", autocolor)
# Mark "autocolor" as our active render layer

20
korman/exporter/manager.py

@ -16,6 +16,7 @@
import bpy
from pathlib import Path
from PyHSPlasma import *
from typing import Iterable
import weakref
from . import explosions
@ -190,6 +191,13 @@ class ExportManager:
else:
return plEncryptedStream.kEncXtea
def find_interfaces(self, pClass, so : plSceneObject) -> Iterable[plObjInterface]:
assert issubclass(pClass, plObjInterface)
for i in (i.object for i in so.interfaces):
if isinstance(i, pClass):
yield i
def find_create_key(self, pClass, bl=None, name=None, so=None):
key = self.find_key(pClass, bl, name, so)
if key is None:
@ -313,16 +321,16 @@ class ExportManager:
with output.generate_dat_file(f, enc=self._encryption) as stream:
fni = bpy.context.scene.world.plasma_fni
stream.writeLine("Graphics.Renderer.SetClearColor {} {} {}".format(*fni.clear_color))
stream.writeLine("Graphics.Renderer.SetYon {}".format(fni.yon))
stream.writeLine("Graphics.Renderer.SetClearColor {:.2f} {:.2f} {:.2f}".format(*fni.clear_color))
stream.writeLine("Graphics.Renderer.SetYon {:.1f}".format(fni.yon))
if fni.fog_method == "none":
stream.writeLine("Graphics.Renderer.Fog.SetDefLinear 0 0 0")
else:
stream.writeLine("Graphics.Renderer.Fog.SetDefColor {} {} {}".format(*fni.fog_color))
stream.writeLine("Graphics.Renderer.Fog.SetDefColor {:.2f} {:.2f} {:.2f}".format(*fni.fog_color))
if fni.fog_method == "linear":
stream.writeLine("Graphics.Renderer.Fog.SetDefLinear {} {} {}".format(fni.fog_start, fni.fog_end, fni.fog_density))
elif fni.fog_method == "exp2":
stream.writeLine("Graphics.Renderer.Fog.SetDefExp2 {} {}".format(fni.fog_end, fni.fog_density))
stream.writeLine("Graphics.Renderer.Fog.SetDefLinear {:.2f} {:.2f} {:.2f}".format(fni.fog_start, fni.fog_end, fni.fog_density))
elif fni.fog_method == "exp":
stream.writeLine("Graphics.Renderer.Fog.SetDefExp {:.2f} {:.2f}".format(fni.fog_end, fni.fog_density))
def _write_pages(self):
age_name = self._age_info.name

148
korman/exporter/material.py

@ -16,8 +16,10 @@
import bpy
import functools
import math
import mathutils
from pathlib import Path
from PyHSPlasma import *
from typing import Union
import weakref
from .explosions import *
@ -143,7 +145,10 @@ class MaterialConverter:
"NONE": self._export_texture_type_none,
}
self._animation_exporters = {
"ambientCtl": functools.partial(self._export_layer_diffuse_animation, converter=self.get_material_ambient),
"opacityCtl": self._export_layer_opacity_animation,
"preshadeCtl": functools.partial(self._export_layer_diffuse_animation, converter=self.get_material_preshade),
"runtimeCtl": functools.partial(self._export_layer_diffuse_animation, converter=self.get_material_runtime),
"transformCtl": self._export_layer_transform_animation,
}
@ -187,7 +192,9 @@ class MaterialConverter:
self._report.msg("Exporting Material '{}' as single user '{}'", bm.name, mat_name, indent=1)
hgmat = None
else:
mat_name = bm.name
# Ensure that RT-lit objects don't infect the static-lit objects.
mat_prefix = "RTLit_" if bo.plasma_modifiers.lighting.rt_lights else ""
mat_name = "".join((mat_prefix, bm.name))
self._report.msg("Exporting Material '{}'", mat_name, indent=1)
hsgmat = self._mgr.find_key(hsGMaterial, name=mat_name, bl=bo)
if hsgmat is not None:
@ -222,7 +229,8 @@ class MaterialConverter:
if slot.use_stencil:
stencils.append((idx, slot))
else:
tex_layer = self.export_texture_slot(bo, bm, hsgmat, slot, idx)
tex_name = "{}_{}".format(mat_name, slot.name)
tex_layer = self.export_texture_slot(bo, bm, hsgmat, slot, idx, name=tex_name)
if restart_pass_next:
tex_layer.state.miscFlags |= hsGMatState.kMiscRestartPassHere
restart_pass_next = False
@ -249,8 +257,9 @@ class MaterialConverter:
# Plasma makes several assumptions that every hsGMaterial has at least one layer. If this
# material had no Textures, we will need to initialize a default layer
if not hsgmat.layers:
layer = self._mgr.find_create_object(plLayer, name="{}_AutoLayer".format(bm.name), bl=bo)
layer = self._mgr.find_create_object(plLayer, name="{}_AutoLayer".format(mat_name), bl=bo)
self._propagate_material_settings(bo, bm, layer)
layer = self._export_layer_animations(bo, bm, None, 0, layer)
hsgmat.addLayer(layer.key)
# Cache this material for later
@ -349,7 +358,7 @@ class MaterialConverter:
return hsgmat.key
def export_bumpmap_slot(self, bo, bm, hsgmat, slot, idx):
name = "{}_{}".format(bm.name if bm is not None else bo.name, slot.name)
name = "{}_{}".format(hsgmat.key.name, slot.name)
self._report.msg("Exporting Plasma Bumpmap Layers for '{}'", name, indent=2)
# Okay, now we need to make 3 layers for the Du, Dw, and Dv
@ -486,7 +495,7 @@ class MaterialConverter:
layer = self._export_layer_animations(bo, bm, slot, idx, layer)
return layer
def _export_layer_animations(self, bo, bm, tex_slot, idx, base_layer):
def _export_layer_animations(self, bo, bm, tex_slot, idx, base_layer) -> plLayer:
"""Exports animations on this texture and chains the Plasma layers as needed"""
def harvest_fcurves(bl_id, collection, data_path=None):
@ -505,9 +514,16 @@ class MaterialConverter:
return None
fcurves = []
texture = tex_slot.texture
mat_action = harvest_fcurves(bm, fcurves, "texture_slots[{}]".format(idx))
tex_action = harvest_fcurves(texture, fcurves)
# Base layers get all of the fcurves for animating things like the diffuse color
texture = tex_slot.texture if tex_slot is not None else None
if idx == 0:
harvest_fcurves(bm, fcurves)
harvest_fcurves(texture, fcurves)
elif tex_slot is not None:
harvest_fcurves(bm, fcurves, tex_slot.path_from_id())
harvest_fcurves(texture, fcurves)
if not fcurves:
return base_layer
@ -515,10 +531,9 @@ class MaterialConverter:
# and chain this biotch up as best we can.
layer_animation = None
for attr, converter in self._animation_exporters.items():
ctrl = converter(tex_slot, base_layer, fcurves)
ctrl = converter(bo, bm, tex_slot, base_layer, fcurves)
if ctrl is not None:
if layer_animation is None:
name = "{}_LayerAnim".format(base_layer.key.name)
layer_animation = self.get_texture_animation_key(bo, bm, texture).object
setattr(layer_animation, attr, ctrl)
@ -531,28 +546,44 @@ class MaterialConverter:
atc = layer_animation.timeConvert
# Since we are harvesting from the material action but are exporting to a layer, the
# action's range is relatively useless. We'll figure our own.
start, end = functools.reduce(lambda x, y: (min(x[0], y[0]), max(x[1], y[1])),
(fcurve.range() for fcurve in fcurves))
atc.begin = start / fps
atc.end = end / fps
layer_props = tex_slot.texture.plasma_layer
if not layer_props.anim_auto_start:
atc.flags |= plAnimTimeConvert.kStopped
if layer_props.anim_loop:
# action's range is relatively useless. We'll figure our own. Reminder: the blender
# documentation is wrong -- FCurve.range() returns a sequence of frame numbers, not times.
atc.begin = min((fcurve.range()[0] for fcurve in fcurves)) * (30.0 / fps) / fps
atc.end = max((fcurve.range()[1] for fcurve in fcurves)) * (30.0 / fps) / fps
if tex_slot is not None:
layer_props = tex_slot.texture.plasma_layer
if not layer_props.anim_auto_start:
atc.flags |= plAnimTimeConvert.kStopped
if layer_props.anim_loop:
atc.flags |= plAnimTimeConvert.kLoop
atc.loopBegin = atc.begin
atc.loopEnd = atc.end
if layer_props.anim_sdl_var:
layer_animation.varName = layer_props.anim_sdl_var
else:
# Hmm... I wonder what we should do here? A reasonable default might be to just
# run the stupid thing in a loop.
atc.flags |= plAnimTimeConvert.kLoop
atc.loopBegin = atc.begin
atc.loopEnd = atc.end
if layer_props.anim_sdl_var:
layer_animation.varName = layer_props.anim_sdl_var
return layer_animation
# Well, we had some FCurves but they were garbage... Too bad.
return base_layer
def _export_layer_opacity_animation(self, tex_slot, base_layer, fcurves):
def _export_layer_diffuse_animation(self, bo, bm, tex_slot, base_layer, fcurves, converter):
assert converter is not None
def translate_color(color_sequence):
# See things like get_material_preshade
result = converter(bo, bm, mathutils.Color(color_sequence))
return result.red, result.green, result.blue
ctrl = self._exporter().animation.make_pos_controller(fcurves, "diffuse_color", bm.diffuse_color, translate_color)
return ctrl
def _export_layer_opacity_animation(self, bo, bm, tex_slot, base_layer, fcurves):
for i in fcurves:
if i.data_path == "plasma_layer.opacity":
base_layer.state.blendFlags |= hsGMatState.kBlendAlpha
@ -560,14 +591,16 @@ class MaterialConverter:
return ctrl
return None
def _export_layer_transform_animation(self, tex_slot, base_layer, fcurves):
path = tex_slot.path_from_id()
pos_path = "{}.offset".format(path)
scale_path = "{}.scale".format(path)
def _export_layer_transform_animation(self, bo, bm, tex_slot, base_layer, fcurves):
if tex_slot is not None:
path = tex_slot.path_from_id()
pos_path = "{}.offset".format(path)
scale_path = "{}.scale".format(path)
# Plasma uses the controller to generate a matrix44... so we have to produce a leaf controller
ctrl = self._exporter().animation.make_matrix44_controller(fcurves, pos_path, scale_path, tex_slot.offset, tex_slot.scale)
return ctrl
# Plasma uses the controller to generate a matrix44... so we have to produce a leaf controller
ctrl = self._exporter().animation.make_matrix44_controller(fcurves, pos_path, scale_path, tex_slot.offset, tex_slot.scale)
return ctrl
return None
def _export_texture_type_environment_map(self, bo, layer, slot):
"""Exports a Blender EnvironmentMapTexture to a plLayer"""
@ -1163,22 +1196,47 @@ class MaterialConverter:
def get_bump_layer(self, bo):
return self._bump_mats.get(bo, None)
def get_material_ambient(self, bo, bm, color : Union[None, mathutils.Color]=None) -> hsColorRGBA:
emit_scale = bm.emit * 0.5
if emit_scale > 0.0:
if color is None:
color = bm.diffuse_color
return hsColorRGBA(color.r * emit_scale,
color.g * emit_scale,
color.b * emit_scale,
1.0)
else:
return utils.color(bpy.context.scene.world.ambient_color)
def get_material_preshade(self, bo, bm, color : Union[None, mathutils.Color]=None) -> hsColorRGBA:
if bo.plasma_modifiers.lighting.rt_lights:
return hsColorRGBA.kBlack
if color is None:
color = bm.diffuse_color
return utils.color(color)
def get_material_runtime(self, bo, bm, color : Union[None, mathutils.Color]=None) -> hsColorRGBA:
if not bo.plasma_modifiers.lighting.preshade:
return hsColorRGBA.kBlack
if color is None:
color = bm.diffuse_color
return utils.color(color)
def get_texture_animation_key(self, bo, bm, texture):
"""Finds or creates the appropriate key for sending messages to an animated Texture"""
tex_name = texture.name
tex_name = texture.name if texture is not None else "AutoLayer"
if bo.type == "LAMP":
assert bm is None
bm_name = bo.name
else:
assert bm is not None
bm_name = bm.name
if not tex_name in bm.texture_slots:
if texture is not None and not tex_name in bm.texture_slots:
raise ExportError("Texture '{}' not used in Material '{}'".format(bm_name, tex_name))
name = "{}_{}_LayerAnim".format(bm_name, tex_name)
layer = texture.plasma_layer
pClass = plLayerSDLAnimation if layer.anim_sdl_var else plLayerAnimation
pClass = plLayerSDLAnimation if texture is not None and texture.plasma_layer.anim_sdl_var else plLayerAnimation
return self._mgr.find_create_key(pClass, bl=bo, name=name)
@property
@ -1204,23 +1262,17 @@ class MaterialConverter:
if bm.use_shadeless:
state.shadeFlags |= hsGMatState.kShadeWhite
if bm.emit:
state.shadeFlags |= hsGMatState.kShadeEmissive
# Colors
layer.ambient = utils.color(bpy.context.scene.world.ambient_color)
layer.preshade = utils.color(bm.diffuse_color)
layer.runtime = utils.color(bm.diffuse_color)
layer.ambient = self.get_material_ambient(bo, bm)
layer.preshade = self.get_material_preshade(bo, bm)
layer.runtime = self.get_material_runtime(bo, bm)
layer.specular = utils.color(bm.specular_color)
layer.specularPower = min(100.0, float(bm.specular_hardness))
layer.LODBias = -1.0 # Seems to be the Plasma default
if bm.emit > 0.0:
# Use the diffuse colour as the emit, scaled by the emit amount
# (maximum 2.0, so we'll also scale that by 0.5)
emit_scale = bm.emit * 0.5
layer.ambient = hsColorRGBA(bm.diffuse_color.r * emit_scale,
bm.diffuse_color.g * emit_scale,
bm.diffuse_color.b * emit_scale,
1.0)
layer.LODBias = -1.0
def _requires_single_user(self, bo, bm):
if bo.data.show_double_sided:

231
korman/exporter/mesh.py

@ -14,6 +14,7 @@
# along with Korman. If not, see <http://www.gnu.org/licenses/>.
import bpy
import itertools
from PyHSPlasma import *
from math import fabs
import weakref
@ -29,6 +30,29 @@ _WARN_VERTS_PER_SPAN = 0x8000
_VERTEX_COLOR_LAYERS = {"col", "color", "colour"}
class _GeoSpan:
def __init__(self, bo, bm, geospan, pass_index=None):
self.geospan = geospan
self.pass_index = pass_index if pass_index is not None else 0
self.mult_color = self._determine_mult_color(bo, bm)
def _determine_mult_color(self, bo, bm):
"""Determines the color all vertex colors should be multipled by in this span."""
if self.geospan.props & plGeometrySpan.kDiffuseFoldedIn:
color = bm.diffuse_color
base_layer = self._find_bottom_of_stack()
return (color.r, color.b, color.g, base_layer.opacity)
if not bo.plasma_modifiers.lighting.preshade:
return (0.0, 0.0, 0.0, 0.0)
return (1.0, 1.0, 1.0, 1.0)
def _find_bottom_of_stack(self) -> plLayerInterface:
base_layer = self.geospan.material.object.layers[0].object
while base_layer.underLay is not None:
base_layer = base_layer.underLay.object
return base_layer
class _RenderLevel:
MAJOR_OPAQUE = 0
MAJOR_FRAMEBUF = 1
@ -39,17 +63,13 @@ class _RenderLevel:
_MAJOR_SHIFT = 28
_MINOR_MASK = ((1 << _MAJOR_SHIFT) - 1)
def __init__(self, bo, hsgmat, pass_index, blend_span=False):
self.level = 0
if pass_index > 0:
self.major = self.MAJOR_FRAMEBUF
self.minor = pass_index * 4
def __init__(self, bo, pass_index, blend_span=False):
if blend_span:
self.level = self._determine_level(bo, blend_span)
else:
self.major = self.MAJOR_BLEND if blend_span else self.MAJOR_OPAQUE
# We use the blender material's pass index (which we stashed in the hsGMaterial) to increment
# the render pass, just like it says...
self.level += pass_index
self.level = 0
# Gulp... Hope you know what you're doing...
self.minor += pass_index * 4
def __eq__(self, other):
return self.level == other.level
@ -60,19 +80,42 @@ class _RenderLevel:
def _get_major(self):
return self.level >> self._MAJOR_SHIFT
def _set_major(self, value):
self.level = ((value << self._MAJOR_SHIFT) & 0xFFFFFFFF) | self.minor
self.level = self._calc_level(value, self.minor)
major = property(_get_major, _set_major)
def _get_minor(self):
return self.level & self._MINOR_MASK
def _set_minor(self, value):
self.level = ((self.major << self._MAJOR_SHIFT) & 0xFFFFFFFF) | value
self.level = self._calc_level(self.major, value)
minor = property(_get_minor, _set_minor)
def _calc_level(self, major : int, minor : int=0) -> int:
return ((major << self._MAJOR_SHIFT) & 0xFFFFFFFF) | minor
def _determine_level(self, bo : bpy.types.Object, blend_span : bool) -> int:
mods = bo.plasma_modifiers
if mods.test_property("draw_framebuf"):
return self._calc_level(self.MAJOR_FRAMEBUF)
elif mods.test_property("draw_opaque"):
return self._calc_level(self.MAJOR_OPAQUE)
elif mods.test_property("draw_no_defer"):
blend_span = False
blend_mod = mods.blend
if blend_mod.enabled and blend_mod.has_dependencies:
level = self._calc_level(self.MAJOR_FRAMEBUF)
for i in blend_mod.iter_dependencies():
level = max(level, self._determine_level(i, blend_span))
return level + 4
elif blend_span:
return self._calc_level(self.MAJOR_BLEND)
else:
return self._calc_level(self.MAJOR_DEFAULT)
class _DrawableCriteria:
def __init__(self, bo, hsgmat, pass_index):
self.blend_span = bool(hsgmat.layers[0].object.state.blendFlags & hsGMatState.kBlendMask)
def __init__(self, bo, geospan, pass_index):
self.blend_span = bool(geospan.props & plGeometrySpan.kRequiresBlending)
self.criteria = 0
if self.blend_span:
@ -80,7 +123,7 @@ class _DrawableCriteria:
self.criteria |= plDrawable.kCritSortFaces
if self._span_sort_allowed(bo):
self.criteria |= plDrawable.kCritSortSpans
self.render_level = _RenderLevel(bo, hsgmat, pass_index, self.blend_span)
self.render_level = _RenderLevel(bo, pass_index, self.blend_span)
def __eq__(self, other):
if not isinstance(other, _DrawableCriteria):
@ -96,12 +139,12 @@ class _DrawableCriteria:
def _face_sort_allowed(self, bo):
# For now, only test the modifiers
# This will need to be tweaked further for GUIs...
return not any((i.no_face_sort for i in bo.plasma_modifiers.modifiers))
return not bo.plasma_modifiers.test_property("no_face_sort")
def _span_sort_allowed(self, bo):
# For now, only test the modifiers
# This will need to be tweaked further for GUIs...
return not any((i.no_face_sort for i in bo.plasma_modifiers.modifiers))
return not bo.plasma_modifiers.test_property("no_face_sort")
@property
def span_type(self):
@ -118,7 +161,6 @@ class _GeoData:
self.vertices = []
class _MeshManager:
def __init__(self, report=None):
if report is not None:
@ -174,11 +216,12 @@ class _MeshManager:
trash_mesh, bo.data = bo.data, data_meshes.get(override["mesh"])
data_meshes.remove(trash_mesh)
# If modifiers were removed, reapply them now.
# If modifiers were removed, reapply them now unless they're read-only.
readonly_attributes = {("DECIMATE", "face_count"),}
for cached_mod in override["modifiers"]:
mod = bo.modifiers.new(cached_mod["name"], cached_mod["type"])
for key, value in cached_mod.items():
if key in {"name", "type"}:
if key in {"name", "type"} or (cached_mod["type"], key) in readonly_attributes:
continue
setattr(mod, key, value)
@ -213,7 +256,49 @@ class MeshConverter(_MeshManager):
return (num_user_texs, total_texs, max_user_texs)
def _create_geospan(self, bo, mesh, bm, hsgmatKey):
def _check_vtx_alpha(self, mesh, material_idx):
if material_idx is not None:
polygons = (i for i in mesh.polygons if i.material_index == material_idx)
else:
polygons = mesh.polygons
alpha_layer = self._find_vtx_alpha_layer(mesh.vertex_colors)
if alpha_layer is None:
return False
alpha_loops = (alpha_layer[i.loop_start:i.loop_start+i.loop_total] for i in polygons)
opaque = (sum(i.color) == len(i.color) for i in itertools.chain.from_iterable(alpha_loops))
has_alpha = not all(opaque)
return has_alpha
def _check_vtx_nonpreshaded(self, bo, mesh, material_idx, base_layer):
def check_layer_shading_animation(layer):
if isinstance(layer, plLayerAnimationBase):
return layer.opacityCtl is not None or layer.preshadeCtl is not None or layer.runtimeCtl is not None
if layer.underLay is not None:
return check_layer_shading_animation(layer.underLay.object)
return False
# TODO: if this is an avatar, we can't be non-preshaded.
if check_layer_shading_animation(base_layer):
return False
# Reject emissive and shadeless because the kLiteMaterial equation has lots of options
# that are taken away by VtxNonPreshaded that are useful here.
if material_idx is not None:
bm = mesh.materials[material_idx]
if bm.emit or bm.use_shadeless:
return False
mods = bo.plasma_modifiers
if mods.lighting.rt_lights:
return True
if mods.lightmap.bake_lightmap:
return True
if self._check_vtx_alpha(mesh, material_idx):
return True
return False
def _create_geospan(self, bo, mesh, material_idx, bm, hsgmatKey):
"""Initializes a plGeometrySpan from a Blender Object and an hsGMaterial"""
geospan = plGeometrySpan()
geospan.material = hsgmatKey
@ -225,10 +310,22 @@ class MeshConverter(_MeshManager):
raise explosions.TooManyUVChannelsError(bo, bm, user_uvws, max_user_uvws)
geospan.format = total_uvws
# Begin total guesswork WRT flags
mods = bo.plasma_modifiers
if mods.lightmap.enabled:
def is_alpha_blended(layer):
if layer.state.blendFlags & hsGMatState.kBlendMask:
return True
if layer.underLay is not None:
return is_alpha_blended(layer.underLay.object)
return False
base_layer = hsgmatKey.object.layers[0].object
if is_alpha_blended(base_layer) or self._check_vtx_alpha(mesh, material_idx):
geospan.props |= plGeometrySpan.kRequiresBlending
if self._check_vtx_nonpreshaded(bo, mesh, material_idx, base_layer):
geospan.props |= plGeometrySpan.kLiteVtxNonPreshaded
if (geospan.props & plGeometrySpan.kLiteMask) != plGeometrySpan.kLiteMaterial:
geospan.props |= plGeometrySpan.kDiffuseFoldedIn
mods = bo.plasma_modifiers
if mods.lighting.rt_lights:
geospan.props |= plGeometrySpan.kPropRunTimeLight
if not bm.use_shadows:
@ -270,7 +367,7 @@ class MeshConverter(_MeshManager):
dspan.composeGeometry(True, True)
inc_progress()
def _export_geometry(self, bo, mesh, materials, geospans):
def _export_geometry(self, bo, mesh, materials, geospans, mat2span_LUT):
# Recall that materials is a mapping of exported materials to blender material indices.
# Therefore, geodata maps blender material indices to working geometry data.
# Maybe the logic is a bit inverted, but it keeps the inner loop simple.
@ -279,15 +376,8 @@ class MeshConverter(_MeshManager):
# Locate relevant vertex color layers now...
lm = bo.plasma_modifiers.lightmap
color, alpha = None, None
for vcol_layer in mesh.tessface_vertex_colors:
name = vcol_layer.name.lower()
if name in _VERTEX_COLOR_LAYERS:
color = vcol_layer.data
elif name == "autocolor" and color is None and not lm.bake_lightmap:
color = vcol_layer.data
elif name == "alpha":
alpha = vcol_layer.data
color = None if lm.bake_lightmap else self._find_vtx_color_layer(mesh.tessface_vertex_colors)
alpha = self._find_vtx_alpha_layer(mesh.tessface_vertex_colors)
# Convert Blender faces into things we can stuff into libHSPlasma
for i, tessface in enumerate(mesh.tessfaces):
@ -317,10 +407,8 @@ class MeshConverter(_MeshManager):
else:
src = alpha[i]
# average color becomes the alpha value
tessface_alphas = (((src.color1[0] + src.color1[1] + src.color1[2]) / 3),
((src.color2[0] + src.color2[1] + src.color2[2]) / 3),
((src.color3[0] + src.color3[1] + src.color3[2]) / 3),
((src.color4[0] + src.color4[1] + src.color4[2]) / 3))
tessface_alphas = ((sum(src.color1) / 3), (sum(src.color2) / 3),
(sum(src.color3) / 3), (sum(src.color4) / 3))
if bumpmap is not None:
gradPass = []
@ -350,9 +438,16 @@ class MeshConverter(_MeshManager):
for j, vertex in enumerate(tessface.vertices):
uvws = tuple([uvw[j] for uvw in tessface_uvws])
# Grab VCols
vertex_color = (int(tessface_colors[j][0] * 255), int(tessface_colors[j][1] * 255),
int(tessface_colors[j][2] * 255), int(tessface_alphas[j] * 255))
# Calculate vertex colors.
if mat2span_LUT:
mult_color = geospans[mat2span_LUT[tessface.material_index]].mult_color
else:
mult_color = (1.0, 1.0, 1.0, 1.0)
tessface_color, tessface_alpha = tessface_colors[j], tessface_alphas[j]
vertex_color = (int(tessface_color[0] * mult_color[0] * 255),
int(tessface_color[1] * mult_color[1] * 255),
int(tessface_color[2] * mult_color[2] * 255),
int(tessface_alpha * mult_color[0] * 255))
# Now, we'll index into the vertex dict using the per-face elements :(
# We're using tuples because lists are not hashable. The many mathutils and PyHSPlasma
@ -409,7 +504,7 @@ class MeshConverter(_MeshManager):
# Time to finish it up...
for i, data in enumerate(geodata.values()):
geospan = geospans[i][0]
geospan = geospans[i].geospan
numVerts = len(data.vertices)
numUVs = geospan.format & plGeometrySpan.kUVCountMask
@ -480,7 +575,7 @@ class MeshConverter(_MeshManager):
# Sequence of tuples (material_index, material)
return sorted(((i, material_source[i]) for i in valid_materials), key=lambda x: x[0])
def export_object(self, bo):
def export_object(self, bo, so : plSceneObject):
# If this object has modifiers, then it's a unique mesh, and we don't need to try caching it
# Otherwise, let's *try* to share meshes as best we can...
if bo.modifiers:
@ -492,7 +587,7 @@ class MeshConverter(_MeshManager):
# Create the DrawInterface
if drawables:
diface = self._mgr.find_create_object(plDrawInterface, bl=bo)
diface = self._mgr.find_create_object(plDrawInterface, bl=bo, so=so)
for dspan_key, idx in drawables:
diface.addDrawable(dspan_key, idx)
@ -512,18 +607,18 @@ class MeshConverter(_MeshManager):
return None
# Step 1: Export all of the doggone materials.
geospans = self._export_material_spans(bo, mesh, materials)
geospans, mat2span_LUT = self._export_material_spans(bo, mesh, materials)
# Step 2: Export Blender mesh data to Plasma GeometrySpans
self._export_geometry(bo, mesh, materials, geospans)
self._export_geometry(bo, mesh, materials, geospans, mat2span_LUT)
# Step 3: Add plGeometrySpans to the appropriate DSpan and create indices
_diindices = {}
for geospan, pass_index in geospans:
dspan = self._find_create_dspan(bo, geospan.material.object, pass_index)
for i in geospans:
dspan = self._find_create_dspan(bo, i.geospan, i.pass_index)
self._report.msg("Exported hsGMaterial '{}' geometry into '{}'",
geospan.material.name, dspan.key.name, indent=1)
idx = dspan.addSourceSpan(geospan)
i.geospan.material.name, dspan.key.name, indent=1)
idx = dspan.addSourceSpan(i.geospan)
diidx = _diindices.setdefault(dspan, [])
diidx.append(idx)
@ -543,22 +638,27 @@ class MeshConverter(_MeshManager):
if len(materials) > 1:
msg = "'{}' is a WaveSet -- only one material is supported".format(bo.name)
self._exporter().report.warn(msg, indent=1)
matKey = self.material.export_waveset_material(bo, materials[0][1])
geospan = self._create_geospan(bo, mesh, materials[0][1], matKey)
blmat = materials[0][1]
matKey = self.material.export_waveset_material(bo, blmat)
geospan = self._create_geospan(bo, mesh, None, blmat, matKey)
# FIXME: Can some of this be generalized?
geospan.props |= (plGeometrySpan.kWaterHeight | plGeometrySpan.kLiteVtxNonPreshaded |
plGeometrySpan.kPropReverseSort | plGeometrySpan.kPropNoShadow)
geospan.waterHeight = bo.location[2]
return [(geospan, 0)]
return [_GeoSpan(bo, blmat, geospan)], None
else:
geospans = [None] * len(materials)
for i, (_, blmat) in enumerate(materials):
mat2span_LUT = {}
for i, (blmat_idx, blmat) in enumerate(materials):
matKey = self.material.export_material(bo, blmat)
geospans[i] = (self._create_geospan(bo, mesh, blmat, matKey), blmat.pass_index)
return geospans
geospans[i] = _GeoSpan(bo, blmat,
self._create_geospan(bo, mesh, blmat_idx, blmat, matKey),
blmat.pass_index)
mat2span_LUT[blmat_idx] = i
return geospans, mat2span_LUT
def _find_create_dspan(self, bo, hsgmat, pass_index):
def _find_create_dspan(self, bo, geospan, pass_index):
location = self._mgr.get_location(bo)
if location not in self._dspans:
self._dspans[location] = {}
@ -569,7 +669,7 @@ class MeshConverter(_MeshManager):
# SortFaces: means we should sort the faces in this span only
# We're using pass index to do just what it was designed for. Cyan has a nicer "depends on"
# draw component, but pass index is the Blender way, so that's what we're doing.
crit = _DrawableCriteria(bo, hsgmat, pass_index)
crit = _DrawableCriteria(bo, geospan, pass_index)
if crit not in self._dspans[location]:
# AgeName_[District_]_Page_RenderLevel_Crit[Blend]Spans
@ -592,6 +692,21 @@ class MeshConverter(_MeshManager):
else:
return self._dspans[location][crit]
def _find_vtx_alpha_layer(self, color_collection):
alpha_layer = next((i for i in color_collection if i.name.lower() == "alpha"), None)
if alpha_layer is not None:
return alpha_layer.data
return None
def _find_vtx_color_layer(self, color_collection):
manual_layer = next((i for i in color_collection if i.name.lower() in _VERTEX_COLOR_LAYERS), None)
if manual_layer is not None:
return manual_layer.data
baked_layer = color_collection.get("autocolor")
if baked_layer is not None:
return baked_layer.data
return None
@property
def _mgr(self):
return self._exporter().mgr

3
korman/exporter/outfile.py

@ -249,7 +249,8 @@ class OutputFiles:
backing_stream = stream
# No sense in wasting time encrypting data that isn't going to be used in the export
if not bogus:
# Also, don't encrypt any MOUL files at all.
if not bogus and self._version != pvMoul:
enc = kwargs.get("enc", None)
if enc is not None:
stream = plEncryptedStream(self._version)

64
korman/exporter/physics.py

@ -60,9 +60,7 @@ class PhysicsConverter:
indices += (v[0], v[2], v[3],)
return indices
def _convert_mesh_data(self, bo, physical, local_space, indices=True):
mat = bo.matrix_world
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:
@ -211,29 +209,41 @@ class PhysicsConverter:
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)
# 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:
# 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 = physical.mass > 0.0
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:
local_space = self._exporter().has_coordiface(bo)
self._bounds_converters[bounds](bo, physical, local_space)
raise NotImplementedError("ODE physical transform")
self._bounds_converters[bounds](bo, physical, local_space, mat)
else:
simIface = so.sim.object
physical = simIface.physical.object
@ -245,14 +255,14 @@ class PhysicsConverter:
self._apply_props(simIface, physical, kwargs)
def _export_box(self, bo, physical, local_space):
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, indices=False)
vertices = self._convert_mesh_data(bo, physical, local_space, mat, indices=False)
physical.calcBoxBounds(vertices)
def _export_hull(self, bo, physical, local_space):
def _export_hull(self, bo, physical, local_space, mat):
"""Exports convex hull bounds based on the object"""
physical.boundsType = plSimDefs.kHullBounds
@ -260,7 +270,6 @@ class PhysicsConverter:
# 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:
mat = bo.matrix_world
if local_space:
physical.pos = hsVector3(*mat.to_translation())
physical.rot = utils.quaternion(mat.to_quaternion())
@ -273,18 +282,25 @@ class PhysicsConverter:
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):
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, indices=False)
vertices = self._convert_mesh_data(bo, physical, local_space, mat, indices=False)
physical.calcSphereBounds(vertices)
def _export_trimesh(self, bo, physical, local_space):
def _export_trimesh(self, bo, physical, local_space, mat):
"""Exports an object's mesh as exact physical bounds"""
physical.boundsType = plSimDefs.kExplicitBounds
vertices, indices = self._convert_mesh_data(bo, physical, local_space)
# 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

17
korman/exporter/utils.py

@ -92,3 +92,20 @@ def bmesh_object(name : str):
bm.to_mesh(mesh)
finally:
bm.free()
@contextmanager
def temporary_mesh_object(source : bpy.types.Object) -> bpy.types.Object:
"""Creates a temporary mesh object from a nonmesh object that will only exist for the duration
of the context."""
assert source.type != "MESH"
obj = bpy.data.objects.new(source.name, source.to_mesh(bpy.context.scene, True, "RENDER"))
obj.draw_type = "WIRE"
obj.matrix_basis, obj.matrix_world = source.matrix_basis, source.matrix_world
obj.parent = source.parent
bpy.context.scene.objects.link(obj)
try:
yield obj
finally:
bpy.data.objects.remove(obj)

3
korman/idprops.py

@ -127,6 +127,9 @@ def poll_animated_objects(self, value):
def poll_camera_objects(self, value):
return value.type == "CAMERA"
def poll_drawable_objects(self, value):
return value.type == "MESH" and any(value.data.materials)
def poll_empty_objects(self, value):
return value.type == "EMPTY"

15
korman/nodes/node_core.py

@ -181,6 +181,12 @@ class PlasmaNodeBase:
"""Generates valid node sockets that can be linked to a specific socket on this node."""
from .node_deprecated import PlasmaDeprecatedNode
source_socket_props = getattr(self.__class__, "output_sockets", {}) if is_output else \
getattr(self.__class__, "input_sockets", {})
source_socket_def = source_socket_props.get(socket.alias, {})
valid_dest_sockets = source_socket_def.get("valid_link_sockets")
valid_dest_nodes = source_socket_def.get("valid_link_nodes")
for dest_node_cls in bpy.types.Node.__subclasses__():
if not issubclass(dest_node_cls, PlasmaNodeBase) or issubclass(dest_node_cls, PlasmaDeprecatedNode):
continue
@ -193,7 +199,14 @@ class PlasmaNodeBase:
continue
if socket_def.get("hidden") is True:
continue
# Can this socket link to the socket_def on the destination node?
if valid_dest_nodes is not None and dest_node_cls.bl_idname not in valid_dest_nodes:
continue
if valid_dest_sockets is not None and socket_def["type"] not in valid_dest_sockets:
continue
# Can the socket_def on the destination node link to this socket?
valid_source_nodes = socket_def.get("valid_link_nodes")
valid_source_sockets = socket_def.get("valid_link_sockets")
if valid_source_nodes is not None and self.bl_idname not in valid_source_nodes:

52
korman/nodes/node_python.py

@ -256,6 +256,10 @@ class PlasmaPythonFileNode(PlasmaVersionedNode, bpy.types.Node):
def export(self, exporter, bo, so):
pfm = self.get_key(exporter, so).object
# Special PFM-SO handling ahoy - be sure to do it for all objects this PFM is attached to.
# Otherwise, you get non-determinant behavior.
self._export_ancillary_sceneobject(exporter, so)
# No need to continue if the PFM was already generated.
if pfm.filename:
return
@ -276,7 +280,6 @@ class PlasmaPythonFileNode(PlasmaVersionedNode, bpy.types.Node):
# Handle exporting the Python Parameters
attrib_sockets = (i for i in self.inputs if i.is_linked)
for socket in attrib_sockets:
attrib = socket.attribute_type
from_node = socket.links[0].from_node
value = from_node.value if socket.is_simple_value else from_node.get_key(exporter, so)
@ -285,27 +288,42 @@ class PlasmaPythonFileNode(PlasmaVersionedNode, bpy.types.Node):
for i in value:
param = plPythonParameter()
param.id = socket.attribute_id
param.valueType = _attrib2param[attrib]
param.valueType = _attrib2param[socket.attribute_type]
param.value = i
# Key type sanity checking... Because I trust no user.
if not socket.is_simple_value:
if i is None:
msg = "'{}' Node '{}' didn't return a key and therefore will be unavailable to Python".format(
self.id_data.name, from_node.name)
exporter.report.warn(msg, indent=3)
else:
key_type = _attrib_key_types[attrib]
if isinstance(key_type, tuple):
good_key = i.type in key_type
else:
good_key = i.type == key_type
if not good_key:
msg = "'{}' Node '{}' returned an unexpected key type '{}'".format(
self.id_data.name, from_node.name, plFactory.ClassName(i.type))
exporter.report.warn(msg, indent=3)
self._export_key_attrib(exporter, bo, so, i, socket)
pfm.addParameter(param)
def _export_ancillary_sceneobject(self, exporter, so : plSceneObject) -> None:
# Danger: Special case evil ahoy...
# If the key is an object that represents a lamp, we have to assume that the reason it's
# being passed to Python is so it can be turned on/off at will. That means it's technically
# an animated lamp.
for light in exporter.mgr.find_interfaces(plLightInfo, so):
exporter.report.msg("Marking RT light '{}' as animated due to usage in a Python File node",
so.key.name, indent=3)
light.setProperty(plLightInfo.kLPMovable, True)
def _export_key_attrib(self, exporter, bo, so : plSceneObject, key : plKey, socket) -> None:
if key is None:
exporter.report.warn("Attribute '{}' didn't return a key and therefore will be unavailable to Python",
self.id_data.name, socket.links[0].name, indent=3)
return
key_type = _attrib_key_types[socket.attribute_type]
if isinstance(key_type, tuple):
good_key = key.type in key_type
else:
good_key = key.type == key_type
if not good_key:
exporter.report.warn("'{}' Node '{}' returned an unexpected key type '{}'",
self.id_data.name, socket.links[0].from_node.name,
plFactory.ClassName(key.type), indent=3)
if isinstance(key.object, plSceneObject):
self._export_ancillary_sceneobject(exporter, key.object)
def _get_attrib_sockets(self, idx):
for i in self.inputs:
if i.attribute_id == idx:

14
korman/nodes/node_responder.py

@ -291,7 +291,7 @@ class PlasmaResponderStateNode(PlasmaNodeBase, bpy.types.Node):
# Convert the commands
commands = CommandMgr(stateMgr.responder)
for i in self.find_outputs("msgs"):
for i in self._get_child_messages():
# slight optimization--commands attached to states can't wait on other commands
# namely because it's impossible to wait on a command that doesn't exist...
self._generate_command(exporter, so, stateMgr.responder, commands, i)
@ -340,16 +340,24 @@ class PlasmaResponderStateNode(PlasmaNodeBase, bpy.types.Node):
if msgNode.has_callbacks:
commandMgr.add_waitable_node(msgNode)
if msgNode.find_output("msgs"):
if msgNode.has_linked_callbacks:
childWaitOn = commandMgr.add_wait(idx)
msgNode.convert_callback_message(exporter, so, msg, responder.key, childWaitOn)
else:
childWaitOn = waitOn
# Export any linked callback messages
for i in msgNode.find_outputs("msgs"):
for i in self._get_child_messages(msgNode):
self._generate_command(exporter, so, responder, commandMgr, i, childWaitOn)
def _get_child_messages(self, node=None):
"""Returns a list of the message nodes sent by `node`. The list is sorted such that any
messages with callbacks are last in the list, allowing proper wait generation.
"""
if node is None:
node = self
return sorted(node.find_outputs("msgs"), key=lambda x: x.has_callbacks and x.has_linked_callbacks)
class PlasmaRespStateSocket(PlasmaNodeSocketBase, bpy.types.NodeSocket):
bl_color = (0.388, 0.78, 0.388, 1.0)

5
korman/operators/op_mesh.py

@ -28,10 +28,6 @@ class PlasmaMeshOperator:
FLARE_MATERIAL_BASE_NAME = "FLAREGEN"
def store_material_selection(self, value):
if bpy.data.materials.get(value, None):
bpy.context.scene.plasma_scene.last_flare_material = value
class PlasmaAddFlareOperator(PlasmaMeshOperator, bpy.types.Operator):
bl_idname = "mesh.plasma_flare_add"
@ -52,7 +48,6 @@ class PlasmaAddFlareOperator(PlasmaMeshOperator, bpy.types.Operator):
flare_material_name = bpy.props.StringProperty(name="Material",
description="A specially-crafted material to use for this flare",
default=FLARE_MATERIAL_BASE_NAME,
update=store_material_selection,
options=set())
@classmethod

68
korman/operators/op_toolbox.py

@ -16,6 +16,7 @@
import bpy
from bpy.props import *
import pickle
import itertools
class ToolboxOperator:
@classmethod
@ -171,6 +172,37 @@ class PlasmaToggleAllPlasmaObjectsOperator(ToolboxOperator, bpy.types.Operator):
i.plasma_object.enabled = self.enable
return {"FINISHED"}
class PlasmaToggleDoubleSidedOperator(ToolboxOperator, bpy.types.Operator):
bl_idname = "mesh.plasma_toggle_double_sided"
bl_label = "Toggle All Double Sided"
bl_description = "Toggles all meshes to be double sided"
enable = BoolProperty(name="Enable", description="Enable Double Sided")
def execute(self, context):
enable = self.enable
for mesh in bpy.data.meshes:
mesh.show_double_sided = enable
return {"FINISHED"}
class PlasmaToggleDoubleSidedSelectOperator(ToolboxOperator, bpy.types.Operator):
bl_idname = "mesh.plasma_toggle_double_sided_selected"
bl_label = "Toggle Selected Double Sided"
bl_description = "Toggles selected meshes double sided value"
@classmethod
def poll(cls, context):
return super().poll(context) and hasattr(bpy.context, "selected_objects")
def execute(self, context):
mesh_list = [i.data for i in context.selected_objects if i.type == "MESH"]
enable = not all((mesh.show_double_sided for mesh in mesh_list))
for mesh in mesh_list:
mesh.show_double_sided = enable
return {"FINISHED"}
class PlasmaToggleEnvironmentMapsOperator(ToolboxOperator, bpy.types.Operator):
bl_idname = "texture.plasma_toggle_environment_maps"
@ -204,3 +236,39 @@ class PlasmaTogglePlasmaObjectsOperator(ToolboxOperator, bpy.types.Operator):
for i in context.selected_objects:
i.plasma_object.enabled = enable
return {"FINISHED"}
class PlasmaToggleSoundExportOperator(ToolboxOperator, bpy.types.Operator):
bl_idname = "object.plasma_toggle_sound_export"
bl_label = "Toggle Sound Export"
bl_description = "Toggles the Export function of all sound emitters' files"
enable = BoolProperty(name="Enable", description="Sound Export Enable")
def execute(self, context):
enable = self.enable
for i in bpy.data.objects:
if i.plasma_modifiers.soundemit is None:
continue
for sound in i.plasma_modifiers.soundemit.sounds:
sound.package = enable
return {"FINISHED"}
class PlasmaToggleSoundExportSelectedOperator(ToolboxOperator, bpy.types.Operator):
bl_idname = "object.plasma_toggle_sound_export_selected"
bl_label = "Toggle Selected Sound Export"
bl_description = "Toggles the Export function of selected sound emitters' files."
@classmethod
def poll(cls, context):
return super().poll(context) and hasattr(bpy.context, "selected_objects")
def execute(self, context):
enable = not all((i.package for i in itertools.chain.from_iterable(i.plasma_modifiers.soundemit.sounds for i in bpy.context.selected_objects)))
for i in context.selected_objects:
if i.plasma_modifiers.soundemit is None:
continue
for sound in i.plasma_modifiers.soundemit.sounds:
sound.package = enable
return {"FINISHED"}

4
korman/properties/modifiers/__init__.py

@ -66,6 +66,10 @@ class PlasmaModifiers(bpy.types.PropertyGroup):
setattr(cls, i.pl_id, bpy.props.PointerProperty(type=i))
bpy.types.Object.plasma_modifiers = bpy.props.PointerProperty(type=cls)
def test_property(self, property : str) -> bool:
"""Tests a property on all enabled Plasma modifiers"""
return any((getattr(i, property) for i in self.modifiers))
class PlasmaModifierSpec(bpy.types.PropertyGroup):
pass

65
korman/properties/modifiers/anim.py

@ -60,37 +60,52 @@ class PlasmaAnimationModifier(ActionModifier, PlasmaModifierProperties):
description="Marker indicating where the default loop begins")
loop_end = StringProperty(name="Loop End",
description="Marker indicating where the default loop ends")
obj_sdl_anim = StringProperty(name="SDL Animation",
description="Name of the SDL variable to use for this animation",
options=set())
@property
def anim_type(self):
return plAgeGlobalAnim if self.enabled and self.obj_sdl_anim else plATCAnim
def export(self, exporter, bo, so):
action = self.blender_action
atcanim = exporter.mgr.find_create_object(plATCAnim, so=so)
atcanim.autoStart = self.auto_start
atcanim.loop = self.loop
# Simple start and loop info
if action is not None:
markers = action.pose_markers
initial_marker = markers.get(self.initial_marker)
if initial_marker is not None:
atcanim.initial = _convert_frame_time(initial_marker.frame)
else:
atcanim.initial = -1.0
if self.loop:
loop_start = markers.get(self.loop_start)
if loop_start is not None:
atcanim.loopStart = _convert_frame_time(loop_start.frame)
anim_mod = bo.plasma_modifiers.animation
# Do not create the private animation here. The animation converter itself does this
# before we reach this point. If it does not create an animation, then we might create an
# empty animation that crashes Uru.
atcanim = exporter.mgr.find_object(anim_mod.anim_type, so=so)
if atcanim is None:
return
if not isinstance(atcanim, plAgeGlobalAnim):
atcanim.autoStart = self.auto_start
atcanim.loop = self.loop
# Simple start and loop info for ATC
if action is not None:
markers = action.pose_markers
initial_marker = markers.get(self.initial_marker)
if initial_marker is not None:
atcanim.initial = _convert_frame_time(initial_marker.frame)
else:
atcanim.initial = -1.0
if self.loop:
loop_start = markers.get(self.loop_start)
if loop_start is not None:
atcanim.loopStart = _convert_frame_time(loop_start.frame)
else:
atcanim.loopStart = atcanim.start
loop_end = markers.get(self.loop_end)
if loop_end is not None:
atcanim.loopEnd = _convert_frame_time(loop_end.frame)
else:
atcanim.loopEnd = atcanim.end
else:
if self.loop:
atcanim.loopStart = atcanim.start
loop_end = markers.get(self.loop_end)
if loop_end is not None:
atcanim.loopEnd = _convert_frame_time(loop_end.frame)
else:
atcanim.loopEnd = atcanim.end
else:
if self.loop:
atcanim.loopStart = atcanim.start
atcanim.loopEnd = atcanim.end
class AnimGroupObject(idprops.IDPropObjectMixin, bpy.types.PropertyGroup):

20
korman/properties/modifiers/base.py

@ -30,10 +30,30 @@ class PlasmaModifierProperties(bpy.types.PropertyGroup):
def destroyed(self):
pass
@property
def draw_opaque(self):
"""Render geometry before the avatar"""
return False
@property
def draw_framebuf(self):
"""Render geometry after the avatar but before other blended geometry"""
return False
@property
def draw_no_defer(self):
"""Disallow geometry being sorted into a blending span"""
return False
@property
def enabled(self):
return self.display_order >= 0
@property
def face_sort(self):
"""Indicates that the geometry's faces should be sorted by the engine"""
return False
def harvest_actors(self):
return ()

4
korman/properties/modifiers/gui.py

@ -578,6 +578,8 @@ class PlasmaLinkingBookModifier(PlasmaModifierProperties, PlasmaModifierLogicWiz
anim_stage = nodes.new("PlasmaAnimStageNode")
anim_stage.anim_name = "LinkOut"
anim_settings = nodes.new("PlasmaAnimStageSettingsNode")
anim_settings.forward = "kPlayAuto"
anim_settings.stage_advance = "kAdvanceAuto"
anim_stage.link_input(anim_settings, "stage", "stage_settings")
msb = nodes.new("PlasmaMultiStageBehaviorNode")
@ -616,6 +618,8 @@ class PlasmaLinkingBookModifier(PlasmaModifierProperties, PlasmaModifierLogicWiz
anim_stage = nodes.new("PlasmaAnimStageNode")
anim_stage.anim_name = "LinkOut"
anim_settings = nodes.new("PlasmaAnimStageSettingsNode")
anim_settings.forward = "kPlayAuto"
anim_settings.stage_advance = "kAdvanceAuto"
anim_stage.link_input(anim_settings, "stage", "stage_settings")
msb = nodes.new("PlasmaMultiStageBehaviorNode")

6
korman/properties/modifiers/physics.py

@ -19,6 +19,7 @@ from PyHSPlasma import *
from .base import PlasmaModifierProperties
from ...exporter import ExportError
from ... import idprops
# These are the kinds of physical bounds Plasma can work with.
# This sequence is acceptable in any EnumProperty
@ -62,6 +63,11 @@ class PlasmaCollider(PlasmaModifierProperties):
mass = FloatProperty(name="Mass", description="Mass of object in pounds", min=0.0, default=1.0)
start_asleep = BoolProperty(name="Start Asleep", description="Object is not active until influenced by another object", default=False)
proxy_object = PointerProperty(name="Proxy",
description="Object used as the collision geometry",
type=bpy.types.Object,
poll=idprops.poll_mesh_objects)
def export(self, exporter, bo, so):
# All modifier properties are examined by this little stinker...
exporter.physics.generate_physical(bo, so)

94
korman/properties/modifiers/render.py

@ -25,6 +25,98 @@ from ...exporter import utils
from ...exporter.explosions import ExportError
from ... import idprops
class PlasmaBlendOntoObject(bpy.types.PropertyGroup):
blend_onto = PointerProperty(name="Blend Onto",
description="Object to render first",
options=set(),
type=bpy.types.Object,
poll=idprops.poll_drawable_objects)
enabled = BoolProperty(name="Enabled",
default=True,
options=set())
class PlasmaBlendMod(PlasmaModifierProperties):
pl_id = "blend"
bl_category = "Render"
bl_label = "Blending"
bl_description = "Advanced Blending Options"
render_level = EnumProperty(name="Render Pass",
description="Suggested render pass for this object.",
items=[("AUTO", "(Auto)", "Let Korman decide when to render this object."),
("OPAQUE", "Before Avatar", "Prefer for the object to draw before the avatar."),
("FRAMEBUF", "Frame Buffer", "Prefer for the object to draw after the avatar but before other blended objects."),
("BLEND", "Blended", "Prefer for the object to draw after most other geometry in the blended pass.")],
options=set())
sort_faces = EnumProperty(name="Sort Faces",
description="",
items=[("AUTO", "(Auto)", "Let Korman decide if faces should be sorted."),
("ALWAYS", "Always", "Force the object's faces to be sorted."),
("NEVER", "Never", "Force the object's faces to never be sorted.")],
options=set())
dependencies = CollectionProperty(type=PlasmaBlendOntoObject)
active_dependency_index = IntProperty(options={"HIDDEN"})
def export(self, exporter, bo, so):
# What'er you lookin at?
pass
@property
def draw_opaque(self):
return self.render_level == "OPAQUE"
@property
def draw_framebuf(self):
return self.render_level == "FRAMEBUF"
@property
def draw_no_defer(self):
return self.render_level != "BLEND"
@property
def face_sort(self):
return self.sort_faces == "ALWAYS"
@property
def no_face_sort(self):
return self.sort_faces == "NEVER"
@property
def has_dependencies(self):
return bool(self.dependencies)
@property
def has_circular_dependency(self):
return self._check_circular_dependency()
def _check_circular_dependency(self, objects=None):
if objects is None:
objects = set()
elif self.name in objects:
return True
objects.add(self.name)
for i in self.iter_dependencies():
# New deep copy of the set for each dependency, so an object can be reused as a
# dependant's dependant.
this_branch = set(objects)
sub_mod = i.plasma_modifiers.blend
if sub_mod.enabled and sub_mod._check_circular_dependency(this_branch):
return True
return False
def iter_dependencies(self):
for i in (j.blend_onto for j in self.dependencies if j.blend_onto is not None and j.enabled):
yield i
def sanity_check(self):
if self.has_circular_dependency:
raise ExportError("'{}': Circular Render Dependency detected!".format(self.name))
class PlasmaDecalManagerRef(bpy.types.PropertyGroup):
enabled = BoolProperty(name="Enabled",
default=True,
@ -439,6 +531,8 @@ class PlasmaLightingMod(PlasmaModifierProperties):
return True
if self.id_data.plasma_object.has_transform_animation:
return True
if mods.collision.enabled and mods.collision.dynamic:
return True
return False

10
korman/properties/modifiers/sound.py

@ -189,9 +189,13 @@ class PlasmaSound(idprops.IDPropMixin, bpy.types.PropertyGroup):
exporter.output.add_sfx(self._sound)
# There is some bug in the MOUL code that causes a crash if this does not match the expected
# result. There's no sense in debugging that though--the user should never specify
# streaming vs static. That's an implementation detail.
pClass = plWin32StreamingSound if length > 4.0 else plWin32StaticSound
# result. Worse, PotS seems to not like static sounds that are brand-new to it. Possibly because
# it needs to be decompressed outside of game. There's no sense in debugging any of that
# though--the user should never specify streaming vs static. That's an implementation detail.
if exporter.mgr.getVer() != pvMoul and self._sound.plasma_sound.package:
pClass = plWin32StreamingSound
else:
pClass = plWin32StreamingSound if length > 4.0 else plWin32StaticSound
# OK. Any Plasma engine that uses OpenAL (MOUL) is subject to this restriction.
# 3D Positional audio MUST... and I mean MUST... have mono emitters.

2
korman/properties/modifiers/water.py

@ -249,7 +249,7 @@ class PlasmaWaterModifier(idprops.IDPropMixin, PlasmaModifierProperties, bpy.typ
# This is much like what happened in PyPRP
speed = self.wind_speed
matrix = wind_obj.matrix_world
matrix = self.wind_object.matrix_world
wind_dir = hsVector3(matrix[1][0] * speed, matrix[1][1] * speed, matrix[1][2] * speed)
else:
# Stolen shamelessly from PyPRP

10
korman/properties/prop_object.py

@ -19,13 +19,11 @@ from PyHSPlasma import *
class PlasmaObject(bpy.types.PropertyGroup):
def _enabled(self, context):
# This makes me sad
if not self.is_inited:
if not self.is_property_set("page"):
self._init(context)
self.is_inited = True
def _init(self, context):
o = context.object
o = self.id_data
age = context.scene.world.plasma_age
# We want to encourage the pages = layers paradigm.
@ -47,8 +45,8 @@ class PlasmaObject(bpy.types.PropertyGroup):
page = StringProperty(name="Page",
description="Page this object will be exported to")
# Implementation Details
is_inited = BoolProperty(description="INTERNAL: Init proc complete",
# DEAD - leaving in just in case external code uses it
is_inited = BoolProperty(description="DEAD",
default=False,
options={"HIDDEN"})

17
korman/properties/prop_world.py

@ -31,7 +31,7 @@ class PlasmaFni(bpy.types.PropertyGroup):
fog_method = EnumProperty(name="Fog Type",
items=[
("linear", "Linear", "Linear Fog"),
("exp2", "Exponential", "Exponential Fog"),
("exp", "Exponential", "Exponential Fog"),
("none", "None", "Use fog from the previous age")
])
fog_start = FloatProperty(name="Start",
@ -148,6 +148,14 @@ class PlasmaPage(bpy.types.PropertyGroup):
class PlasmaAge(bpy.types.PropertyGroup):
def export(self, exporter):
if exporter.mgr.getVer() == pvMoul:
log_func = exporter.report.warn
else:
log_func = exporter.report.port
if self.seq_prefix <= self.MOUL_PREFIX_RANGE[0] or self.seq_prefix >= self.MOUL_PREFIX_RANGE[1]:
log_func("Age Sequence Prefix {} is potentially out of range (should be between {} and {})",
self.seq_prefix, *self.MOUL_PREFIX_RANGE)
_age_info = plAgeInfo()
_age_info.dayLength = self.day_length
_age_info.lingerTime = 180 # this is fairly standard
@ -157,6 +165,10 @@ class PlasmaAge(bpy.types.PropertyGroup):
_age_info.startDateTime = self.start_time
return _age_info
# Sequence prefix helpers
MOUL_PREFIX_RANGE = ((pow(2, 16) - pow(2, 15)) * -1, pow(2, 15) - 1)
SP_PRFIX_RANGE = ((pow(2, 24) - pow(2, 23)) * -1, pow(2, 23) - 1)
day_length = FloatProperty(name="Day Length",
description="Length of a day (in hours) on this age",
default=30.230000,
@ -169,7 +181,10 @@ class PlasmaAge(bpy.types.PropertyGroup):
min=0)
seq_prefix = IntProperty(name="Sequence Prefix",
description="A unique numerical ID for this age",
min=SP_PRFIX_RANGE[0],
soft_min=0, # Negative indicates global--advanced users only
soft_max=MOUL_PREFIX_RANGE[1],
max=SP_PRFIX_RANGE[1],
default=100)
pages = CollectionProperty(name="Pages",
description="Registry pages for this age",

6
korman/ui/modifiers/anim.py

@ -40,10 +40,12 @@ def animation(modifier, layout, context):
if action:
layout.prop_search(modifier, "initial_marker", action, "pose_markers", icon="PMARKER")
col = layout.column()
col.enabled = modifier.loop
col.enabled = modifier.loop and not modifier.obj_sdl_anim
col.prop_search(modifier, "loop_start", action, "pose_markers", icon="PMARKER")
col.prop_search(modifier, "loop_end", action, "pose_markers", icon="PMARKER")
layout.separator()
layout.prop(modifier, "obj_sdl_anim")
def animation_filter(modifier, layout, context):
split = layout.split()

5
korman/ui/modifiers/physics.py

@ -39,6 +39,11 @@ def collision(modifier, layout, context):
col.active = modifier.dynamic
col.prop(modifier, "mass")
layout.separator()
row = layout.row()
row.active = modifier.bounds == "trimesh"
row.prop(modifier, "proxy_object")
def subworld_def(modifier, layout, context):
layout.prop(modifier, "sub_type")
if modifier.sub_type != "dynamicav":

30
korman/ui/modifiers/render.py

@ -18,6 +18,36 @@ import bpy
from .. import ui_list
from ...exporter.mesh import _VERTEX_COLOR_LAYERS
class BlendOntoListUI(bpy.types.UIList):
def draw_item(self, context, layout, data, item, icon, active_data, active_property, index=0, flt_flag=0):
if item.blend_onto is None:
layout.label("[No Object Specified]", icon="ERROR")
else:
layout.label(item.blend_onto.name, icon="OBJECT_DATA")
layout.prop(item, "enabled", text="")
def blend(modifier, layout, context):
# Warn if there are render dependencies and a manual render level specification -- this
# could lead to unpredictable results.
layout.alert = modifier.render_level != "AUTO" and bool(modifier.dependencies)
layout.prop(modifier, "render_level")
layout.alert = False
layout.prop(modifier, "sort_faces")
layout.separator()
layout.label("Render Dependencies:")
ui_list.draw_modifier_list(layout, "BlendOntoListUI", modifier, "dependencies",
"active_dependency_index", rows=2, maxrows=4)
try:
dependency_ref = modifier.dependencies[modifier.active_dependency_index]
except:
pass
else:
layout.alert = dependency_ref.blend_onto is None
layout.prop(dependency_ref, "blend_onto")
class DecalMgrListUI(bpy.types.UIList):
def draw_item(self, context, layout, data, item, icon, active_data, active_property, index=0, flt_flag=0):
if item.name:

17
korman/ui/ui_menus.py

@ -40,8 +40,25 @@ def build_plasma_menu(self, context):
self.layout.separator()
self.layout.menu("menu.plasma_add", icon="URL")
class PlasmaHelpMenu(PlasmaMenu, bpy.types.Menu):
bl_idname = "menu.plasma_help"
bl_label = "Korman..."
def draw(self, context):
layout = self.layout
layout.operator("wm.url_open", text="About Korman", icon="URL").url = "https://guildofwriters.org/wiki/Korman"
layout.operator("wm.url_open", text="Getting Started", icon="URL").url = "https://guildofwriters.org/wiki/Korman:Getting_Started"
layout.operator("wm.url_open", text="Tutorials", icon="URL").url = "https://guildofwriters.org/wiki/Category:Korman_Tutorials"
def build_plasma_help_menu(self, context):
self.layout.menu("menu.plasma_help", text="Korman", icon="URL")
def register():
bpy.types.INFO_MT_add.append(build_plasma_menu)
bpy.types.INFO_MT_help.prepend(build_plasma_help_menu)
def unregister():
bpy.types.INFO_MT_add.remove(build_plasma_menu)
bpy.types.INFO_MT_help.remove(build_plasma_help_menu)

13
korman/ui/ui_toolbox.py

@ -14,6 +14,7 @@
# along with Korman. If not, see <http://www.gnu.org/licenses/>.
import bpy
import itertools
class ToolboxPanel:
bl_category = "Tools"
@ -44,12 +45,24 @@ class PlasmaToolboxPanel(ToolboxPanel, bpy.types.Panel):
col.label("Plasma Pages:")
col.operator("object.plasma_move_selection_to_page", icon="BOOKMARKS", text="Move to Page")
col.operator("object.plasma_select_page_objects", icon="RESTRICT_SELECT_OFF", text="Select Objects")
col.label("Package Sounds:")
col.operator("object.plasma_toggle_sound_export", icon="MUTE_IPO_OFF", text="Enable All").enable = True
all_sounds_export = all((i.package for i in itertools.chain.from_iterable(i.plasma_modifiers.soundemit.sounds for i in bpy.context.selected_objects if i.plasma_modifiers.soundemit.enabled)))
col.operator("object.plasma_toggle_sound_export_selected", icon="OUTLINER_OB_SPEAKER", text="Disable Selection" if all_sounds_export else "Enable Selection")
col.operator("object.plasma_toggle_sound_export", icon="MUTE_IPO_ON", text="Disable All").enable = False
col.label("Textures:")
col.operator("texture.plasma_enable_all_textures", icon="TEXTURE", text="Enable All")
col.operator("texture.plasma_toggle_environment_maps", icon="IMAGE_RGB", text="Enable All EnvMaps").enable = True
col.operator("texture.plasma_toggle_environment_maps", icon="IMAGE_RGB_ALPHA", text="Disable All EnvMaps").enable = False
# Double Sided Operators
col.label("Double Sided:")
col.operator("mesh.plasma_toggle_double_sided", icon="MESH_DATA", text="Disable All").enable = False
all_double_sided = all((i.data.show_double_sided for i in bpy.context.selected_objects if i.type == "MESH"))
col.operator("mesh.plasma_toggle_double_sided_selected", icon="BORDER_RECT", text="Disable Selection" if all_double_sided else "Enable Selection")
col.label("Convert:")
col.operator("object.plasma_convert_plasma_objects", icon="OBJECT_DATA", text="Plasma Objects")
col.operator("texture.plasma_convert_layer_opacities", icon="IMAGE_RGB_ALPHA", text="Layer Opacities")

23
korman/ui/ui_world.py

@ -210,6 +210,8 @@ class PlasmaAgePanel(AgeButtonsPanel, bpy.types.Panel):
# Age Names should really be legal Python 2.x identifiers for AgeSDLHooks
legal_identifier = korlib.is_legal_python2_identifier(age.age_name)
illegal_age_name = not legal_identifier or '_' in age.age_name
bad_prefix = age.seq_prefix >= age.MOUL_PREFIX_RANGE[1] or age.seq_prefix <= age.MOUL_PREFIX_RANGE[0]
# Core settings
layout.separator()
@ -222,17 +224,28 @@ class PlasmaAgePanel(AgeButtonsPanel, bpy.types.Panel):
col = split.column()
col.label("Age Settings:")
col.alert = bad_prefix
col.prop(age, "seq_prefix", text="ID")
col.alert = not legal_identifier or '_' in age.age_name
col.alert = illegal_age_name
col.prop(age, "age_name", text="")
if age.seq_prefix >= age.MOUL_PREFIX_RANGE[1]:
layout.label(text="Your sequence prefix is too high for Myst Online: Uru Live", icon="ERROR")
elif age.seq_prefix <= age.MOUL_PREFIX_RANGE[0]:
# Unlikely.
layout.label(text="Your sequence prefix is too low for Myst Online: Uru Live", icon="ERROR")
# Display a hint if the identifier is illegal
if not legal_identifier:
if korlib.is_python_keyword(age.age_name):
if illegal_age_name:
if not age.age_name:
layout.label(text="Age names cannot be empty", icon="ERROR")
elif korlib.is_python_keyword(age.age_name):
layout.label(text="Ages should not be named the same as a Python keyword", icon="ERROR")
elif age.age_sdl:
fixed_identifier = korlib.replace_python2_identifier(age.age_name)
layout.label(text="Age's SDL will use the name '{}'".format(fixed_identifier), icon="ERROR")
if '_' in age.age_name:
layout.label(text="Age names should not contain underscores", icon="ERROR")
layout.separator()
split = layout.split()
@ -262,6 +275,10 @@ class PlasmaEnvironmentPanel(AgeButtonsPanel, bpy.types.Panel):
layout = self.layout
fni = context.world.plasma_fni
# warn about reversed linear fog values
if fni.fog_method == "linear" and fni.fog_start >= fni.fog_end and (fni.fog_start + fni.fog_end) != 0:
layout.label(text="Fog Start Value should be less than the End Value", icon="ERROR")
# basic colors
split = layout.split()
col = split.column()

Loading…
Cancel
Save