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. 547
      korman/exporter/animation.py
  2. 3
      korman/exporter/camera.py
  3. 31
      korman/exporter/convert.py
  4. 5
      korman/exporter/etlight.py
  5. 20
      korman/exporter/manager.py
  6. 122
      korman/exporter/material.py
  7. 231
      korman/exporter/mesh.py
  8. 3
      korman/exporter/outfile.py
  9. 62
      korman/exporter/physics.py
  10. 17
      korman/exporter/utils.py
  11. 3
      korman/idprops.py
  12. 13
      korman/nodes/node_core.py
  13. 48
      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. 19
      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. 8
      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. 4
      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

547
korman/exporter/animation.py

@ -14,10 +14,13 @@
# along with Korman. If not, see <http://www.gnu.org/licenses/>. # along with Korman. If not, see <http://www.gnu.org/licenses/>.
import bpy import bpy
from collections import defaultdict
import functools
import itertools import itertools
import math import math
import mathutils import mathutils
from PyHSPlasma import * from PyHSPlasma import *
from typing import *
import weakref import weakref
from . import utils from . import utils
@ -27,10 +30,10 @@ class AnimationConverter:
self._exporter = weakref.ref(exporter) self._exporter = weakref.ref(exporter)
self._bl_fps = bpy.context.scene.render.fps 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 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: if not bo.plasma_object.has_animation_data:
return return
@ -72,7 +75,8 @@ class AnimationConverter:
# There is a race condition in the client with animation loading. It expects for modifiers # 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. # 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) 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 # Add the animation data to the ATC
for i in applicators: for i in applicators:
@ -89,14 +93,16 @@ class AnimationConverter:
if i is not None: if i is not None:
yield i.frame_range[index] yield i.frame_range[index]
atcanim.name = "(Entire Animation)" 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.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))) atcanim.end = self._convert_frame_time(max(get_ranges(obj_action, data_action, index=1)))
if isinstance(atcanim, plAgeGlobalAnim):
atcanim.globalVarName = anim_mod.obj_sdl_anim
if isinstance(atcanim, plATCAnim):
# Marker points # Marker points
if obj_action is not None: if obj_action is not None:
for marker in obj_action.pose_markers: for marker in obj_action.pose_markers:
atcanim.setMarker(marker.name, self._convert_frame_time(marker.frame)) atcanim.setMarker(marker.name, self._convert_frame_time(marker.frame))
# Fixme? Not sure if we really need to expose this... # Fixme? Not sure if we really need to expose this...
atcanim.easeInMin = 1.0 atcanim.easeInMin = 1.0
atcanim.easeInMax = 1.0 atcanim.easeInMax = 1.0
@ -188,7 +194,7 @@ class AnimationConverter:
return None return None
energy_curve = next((i for i in fcurves if i.data_path == "energy" and i.keyframe_points), 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: if energy_curve is None and color_curves is None:
return None return None
elif lamp.use_only_shadow: elif lamp.use_only_shadow:
@ -199,10 +205,15 @@ class AnimationConverter:
return None return None
# OK Specular is easy. We just toss out the color as a point3. # 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: if color_keyframes and lamp.use_specular:
channel = plPointControllerChannel() 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 = plLightSpecularApplicator()
applicator.channelName = name applicator.channelName = name
applicator.channel = channel applicator.channel = channel
@ -211,18 +222,20 @@ class AnimationConverter:
# Hey, look, it's a third way to process FCurves. YAY! # Hey, look, it's a third way to process FCurves. YAY!
def convert_diffuse_animation(color, energy): def convert_diffuse_animation(color, energy):
if lamp.use_negative: 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: else:
return { key: value * energy[0] for key, value in color.items() } proc = lambda x: x * energy[0]
diffuse_defaults = { "color": lamp.color, "energy": lamp.energy } 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_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: if not diffuse_keyframes:
return None return None
# Whew. # Whew.
channel = plPointControllerChannel() channel = plPointControllerChannel()
channel.controller = self._make_point3_controller([], diffuse_keyframes, False, []) channel.controller = self._make_point3_controller(diffuse_keyframes, False)
applicator = plLightDiffuseApplicator() applicator = plLightDiffuseApplicator()
applicator.channelName = name applicator.channelName = name
applicator.channel = channel applicator.channel = channel
@ -236,8 +249,16 @@ class AnimationConverter:
distance_fcurve = next((i for i in fcurves if i.data_path == "distance"), None) distance_fcurve = next((i for i in fcurves if i.data_path == "distance"), None)
if energy_fcurve is None and distance_fcurve is None: if energy_fcurve is None and distance_fcurve is None:
return 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 # All types allow animating cutoff
if distance_fcurve is not None: if distance_fcurve is not None:
@ -252,15 +273,9 @@ class AnimationConverter:
falloff = lamp.falloff_type falloff = lamp.falloff_type
if falloff == "CONSTANT": if falloff == "CONSTANT":
if energy_fcurve is not None: 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": elif falloff == "INVERSE_LINEAR":
def convert_linear_atten(distance, energy): keyframes = self._process_fcurves(omni_fcurves, omni_channels, 1, convert_omni_atten, omni_defaults)
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})
if keyframes: if keyframes:
channel = plScalarControllerChannel() channel = plScalarControllerChannel()
channel.controller = self._make_scalar_leaf_controller(keyframes, False) channel.controller = self._make_scalar_leaf_controller(keyframes, False)
@ -270,13 +285,8 @@ class AnimationConverter:
yield applicator yield applicator
elif falloff == "INVERSE_SQUARE": elif falloff == "INVERSE_SQUARE":
if self._mgr.getVer() >= pvMoul: if self._mgr.getVer() >= pvMoul:
def convert_quadratic_atten(distance, energy): report.port("Lamp {} Falloff animations are only supported in Myst Online: Uru Live", falloff, indent=3)
intens = abs(energy[0]) keyframes = self._process_fcurves(omni_fcurves, omni_channels, 1, convert_omni_atten, omni_defaults)
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})
if keyframes: if keyframes:
channel = plScalarControllerChannel() channel = plScalarControllerChannel()
channel.controller = self._make_scalar_leaf_controller(keyframes, False) channel.controller = self._make_scalar_leaf_controller(keyframes, False)
@ -285,19 +295,15 @@ class AnimationConverter:
applicator.channel = channel applicator.channel = channel
yield applicator yield applicator
else: 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: 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): def _convert_sound_volume_animation(self, name, fcurves, soundemit):
if not fcurves: if not fcurves:
return None return None
def convert_volume(value): convert_volume = lambda x: math.log10(max(.01, x / 100.0)) * 20.0
if value == 0.0:
return 0.0
else:
return math.log10(value) * 20.0
for sound in soundemit.sounds: for sound in soundemit.sounds:
path = "{}.volume".format(sound.path_from_id()) path = "{}.volume".format(sound.path_from_id())
@ -341,8 +347,11 @@ class AnimationConverter:
size = spot_size[0] size = spot_size[0]
value = size - (blend * size) value = size - (blend * size)
return math.degrees(value) 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: if keyframes:
channel = plScalarControllerChannel() channel = plScalarControllerChannel()
@ -352,7 +361,7 @@ class AnimationConverter:
applicator.channel = channel applicator.channel = channel
yield applicator 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) tm = self.convert_transform_controller(fcurves, xform, allow_empty)
if tm is None and not allow_empty: if tm is None and not allow_empty:
return None return None
@ -367,13 +376,14 @@ class AnimationConverter:
return applicator 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: if not fcurves and not allow_empty:
return None return None
pos = self.make_pos_controller(fcurves, xform) pos = self.make_pos_controller(fcurves, "location", xform.to_translation())
rot = self.make_rot_controller(fcurves, xform) # TODO: support rotation_quaternion
scale = self.make_scale_controller(fcurves, xform) 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 pos is None and rot is None and scale is None:
if not allow_empty: if not allow_empty:
return None return None
@ -384,17 +394,17 @@ class AnimationConverter:
tm.Z = scale tm.Z = scale
return tm 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) mod = self._mgr.find_create_key(plAGModifier, so=so, bl=bo)
master = self._mgr.find_create_key(plAGMasterMod, so=so, bl=bo) master = self._mgr.find_create_key(plAGMasterMod, so=so, bl=bo)
return mod, master 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) mod = self._mgr.find_create_object(plAGModifier, so=so, bl=bo)
master = self._mgr.find_create_object(plAGMasterMod, so=so, bl=bo) master = self._mgr.find_create_object(plAGMasterMod, so=so, bl=bo)
return mod, master 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? # we might be controlling more than one animation. isn't that cute?
# https://www.youtube.com/watch?v=hspNaoxzNbs # https://www.youtube.com/watch?v=hspNaoxzNbs
# (but obviously this is not wrong...) # (but obviously this is not wrong...)
@ -404,72 +414,65 @@ class AnimationConverter:
else: else:
return self.get_anigraph_keys(bo, so)[1] return self.get_anigraph_keys(bo, so)[1]
def make_matrix44_controller(self, fcurves, pos_path, scale_path, pos_default, scale_default): def make_matrix44_controller(self, fcurves, pos_path : str, scale_path : str, pos_default, scale_default) -> Union[None, plLeafController]:
def convert_matrix_keyframe(**kwargs): def convert_matrix_keyframe(**kwargs) -> hsMatrix44:
pos = kwargs.get(pos_path) pos = kwargs[pos_path]
scale = kwargs.get(scale_path) scale = kwargs[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)
matrix = hsMatrix44() matrix = hsMatrix44()
# Note: scale and pos are dicts, so we can't unpack matrix.setTranslate(hsVector3(*pos))
matrix.setTranslate(hsVector3(allpos[0], allpos[1], allpos[2])) matrix.setScale(hsVector3(*scale))
matrix.setScale(hsVector3(allscale[0], allscale[1], allscale[2]))
return matrix return matrix
fcurves = [i for i in fcurves if i.data_path == pos_path or i.data_path == scale_path] fcurves = [i for i in fcurves if i.data_path == pos_path or i.data_path == scale_path]
if not fcurves: if not fcurves:
return None return None
channels = { pos_path: 3, scale_path: 3 }
default_values = { pos_path: pos_default, scale_path: scale_default } 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: if not keyframes:
return None return None
# Now we make the controller # Now we make the controller
return self._make_matrix44_controller(keyframes) return self._make_matrix44_controller(keyframes)
def make_pos_controller(self, fcurves, default_xform, convert=None): 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 == "location" and i.keyframe_points] 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, convert) keyframes, bez_chans = self._process_keyframes(pos_curves, 3, default_xform, convert)
if not keyframes: if not keyframes:
return None return None
# At one point, I had some... insanity here to try to crush bezier channels and hand off to # 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 :) # 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 return ctrl
def make_rot_controller(self, fcurves, default_xform, convert=None): def make_rot_controller(self, fcurves, data_path : str, default_xform, convert=None) -> Union[None, plCompoundController, plLeafController]:
# TODO: support rotation_quaternion rot_curves = [i for i in fcurves if i.data_path == data_path and i.keyframe_points]
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, 3, default_xform, convert=None)
keyframes, bez_chans = self._process_keyframes(rot_curves, convert=None)
if not keyframes: if not keyframes:
return None return None
# Ugh. Unfortunately, it appears Blender's default interpolation is bezier. So who knows if # 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. # many users will actually see the benefit here? Makes me sad.
if bez_chans: 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: else:
ctrl = self._make_quat_controller(rot_curves, keyframes, default_xform.to_euler()) ctrl = self._make_quat_controller( keyframes)
return ctrl return ctrl
def make_scale_controller(self, fcurves, default_xform, convert=None): 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 == "scale" and i.keyframe_points] 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, convert) keyframes, bez_chans = self._process_keyframes(scale_curves, 3, default_xform, convert)
if not keyframes: if not keyframes:
return None return None
# There is no such thing as a compound scale controller... in Plasma, anyway. # 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 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) keyframes, bezier = self._process_fcurve(fcurve, convert)
if not keyframes: if not keyframes:
return None return None
@ -477,7 +480,7 @@ class AnimationConverter:
ctrl = self._make_scalar_leaf_controller(keyframes, bezier) ctrl = self._make_scalar_leaf_controller(keyframes, bezier)
return ctrl return ctrl
def _make_matrix44_controller(self, keyframes): def _make_matrix44_controller(self, keyframes) -> plLeafController:
ctrl = plLeafController() ctrl = plLeafController()
keyframe_type = hsKeyFrame.kMatrix44KeyFrame keyframe_type = hsKeyFrame.kMatrix44KeyFrame
exported_frames = [] exported_frames = []
@ -487,52 +490,32 @@ class AnimationConverter:
exported.frame = keyframe.frame_num exported.frame = keyframe.frame_num
exported.frameTime = keyframe.frame_time exported.frameTime = keyframe.frame_time
exported.type = keyframe_type exported.type = keyframe_type
exported.value = keyframe.value exported.value = keyframe.values[0]
exported_frames.append(exported) exported_frames.append(exported)
ctrl.keys = (exported_frames, keyframe_type) ctrl.keys = (exported_frames, keyframe_type)
return ctrl return ctrl
def _make_point3_controller(self, fcurves, keyframes, bezier, default_xform): def _make_point3_controller(self, keyframes, bezier) -> plLeafController:
ctrl = plLeafController() ctrl = plLeafController()
subctrls = ("X", "Y", "Z")
keyframe_type = hsKeyFrame.kBezPoint3KeyFrame if bezier else hsKeyFrame.kPoint3KeyFrame keyframe_type = hsKeyFrame.kBezPoint3KeyFrame if bezier else hsKeyFrame.kPoint3KeyFrame
exported_frames = [] exported_frames = []
ctrl_fcurves = { i.array_index: i for i in fcurves }
for keyframe in keyframes: for keyframe in keyframes:
exported = hsPoint3Key() exported = hsPoint3Key()
exported.frame = keyframe.frame_num exported.frame = keyframe.frame_num
exported.frameTime = keyframe.frame_time exported.frameTime = keyframe.frame_time
exported.type = keyframe_type exported.type = keyframe_type
exported.inTan = hsVector3(*keyframe.in_tans)
in_tan = hsVector3() exported.outTan = hsVector3(*keyframe.out_tans)
out_tan = hsVector3() exported.value = hsVector3(*keyframe.values)
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_frames.append(exported) exported_frames.append(exported)
ctrl.keys = (exported_frames, keyframe_type) ctrl.keys = (exported_frames, keyframe_type)
return ctrl return ctrl
def _make_quat_controller(self, fcurves, keyframes, default_xform): def _make_quat_controller(self, keyframes) -> plLeafController:
ctrl = plLeafController() ctrl = plLeafController()
keyframe_type = hsKeyFrame.kQuatKeyFrame keyframe_type = hsKeyFrame.kQuatKeyFrame
exported_frames = [] exported_frames = []
ctrl_fcurves = { i.array_index: i for i in fcurves }
for keyframe in keyframes: for keyframe in keyframes:
exported = hsQuatKey() exported = hsQuatKey()
@ -541,34 +524,21 @@ class AnimationConverter:
exported.type = keyframe_type exported.type = keyframe_type
# NOTE: quat keyframes don't do bezier nonsense # NOTE: quat keyframes don't do bezier nonsense
value = mathutils.Euler() value = mathutils.Euler(keyframe.values)
for i in range(3): exported.value = utils.quaternion(value.to_quaternion())
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)
exported_frames.append(exported) exported_frames.append(exported)
ctrl.keys = (exported_frames, keyframe_type) ctrl.keys = (exported_frames, keyframe_type)
return ctrl 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() ctrl = plCompoundController()
subctrls = ("X", "Y", "Z") subctrls = ("X", "Y", "Z")
for i in subctrls: for i in subctrls:
setattr(ctrl, i, plLeafController()) setattr(ctrl, i, plLeafController())
exported_frames = ([], [], []) exported_frames = ([], [], [])
ctrl_fcurves = { i.array_index: i for i in fcurves }
for keyframe in keyframes: for keyframe in keyframes:
for i, subctrl in enumerate(subctrls): 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 keyframe_type = hsKeyFrame.kBezScalarKeyFrame if i in bez_chans else hsKeyFrame.kScalarKeyFrame
exported = hsScalarKey() exported = hsScalarKey()
exported.frame = keyframe.frame_num exported.frame = keyframe.frame_num
@ -576,23 +546,14 @@ class AnimationConverter:
exported.inTan = keyframe.in_tans[i] exported.inTan = keyframe.in_tans[i]
exported.outTan = keyframe.out_tans[i] exported.outTan = keyframe.out_tans[i]
exported.type = keyframe_type exported.type = keyframe_type
exported.value = fval exported.value = keyframe.values[i]
exported_frames[i].append(exported) exported_frames[i].append(exported)
for i, subctrl in enumerate(subctrls): for i, subctrl in enumerate(subctrls):
my_keyframes = exported_frames[i] 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) getattr(ctrl, subctrl).keys = (my_keyframes, my_keyframes[0].type)
return ctrl return ctrl
def _make_scalar_leaf_controller(self, keyframes, bezier): def _make_scalar_leaf_controller(self, keyframes, bezier) -> plLeafController:
ctrl = plLeafController() ctrl = plLeafController()
keyframe_type = hsKeyFrame.kBezScalarKeyFrame if bezier else hsKeyFrame.kScalarKeyFrame keyframe_type = hsKeyFrame.kBezScalarKeyFrame if bezier else hsKeyFrame.kScalarKeyFrame
exported_frames = [] exported_frames = []
@ -601,239 +562,195 @@ class AnimationConverter:
exported = hsScalarKey() exported = hsScalarKey()
exported.frame = keyframe.frame_num exported.frame = keyframe.frame_num
exported.frameTime = keyframe.frame_time exported.frameTime = keyframe.frame_time
exported.inTan = keyframe.in_tan exported.inTan = keyframe.in_tans[0]
exported.outTan = keyframe.out_tan exported.outTan = keyframe.out_tans[0]
exported.type = keyframe_type exported.type = keyframe_type
exported.value = keyframe.value exported.value = keyframe.values[0]
exported_frames.append(exported) exported_frames.append(exported)
ctrl.keys = (exported_frames, keyframe_type) ctrl.keys = (exported_frames, keyframe_type)
return ctrl return ctrl
def _make_scale_value_controller(self, fcurves, keyframes, bez_chans, default_xform): def _make_scale_value_controller(self, keyframes, bez_chans) -> plLeafController:
subctrls = ("X", "Y", "Z")
keyframe_type = hsKeyFrame.kBezScaleKeyFrame if bez_chans else hsKeyFrame.kScaleKeyFrame keyframe_type = hsKeyFrame.kBezScaleKeyFrame if bez_chans else hsKeyFrame.kScaleKeyFrame
exported_frames = [] exported_frames = []
ctrl_fcurves = { i.array_index: i for i in fcurves }
default_scale = default_xform.to_scale() # Hmm... This smells... But it was basically doing this before the rewrite.
unit_quat = default_xform.to_quaternion() unit_quat = hsQuat(0.0, 0.0, 0.0, 1.0)
unit_quat.normalize()
unit_quat = utils.quaternion(unit_quat)
for keyframe in keyframes: for keyframe in keyframes:
exported = hsScaleKey() exported = hsScaleKey()
exported.frame = keyframe.frame_num exported.frame = keyframe.frame_num
exported.frameTime = keyframe.frame_time exported.frameTime = keyframe.frame_time
exported.type = keyframe_type exported.type = keyframe_type
exported.inTan = hsVector3(*keyframe.in_tans)
in_tan = hsVector3() exported.outTan = hsVector3(*keyframe.out_tans)
out_tan = hsVector3() exported.value = (hsVector3(*keyframe.values), unit_quat)
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_frames.append(exported) exported_frames.append(exported)
ctrl = plLeafController() ctrl = plLeafController()
ctrl.keys = (exported_frames, keyframe_type) ctrl.keys = (exported_frames, keyframe_type)
return ctrl 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""" """Like _process_keyframes, but for one fcurve"""
keyframe_data = type("KeyFrameData", (), {})
fps = self._bl_fps
pi = math.pi
keyframes = {} # Adapt from incoming single item sequence to a single argument.
bezier = False if convert is not None:
fcurve.update() single_convert = lambda x: convert(x[0])
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: else:
keyframe.frame_num = int(frame_num * (30.0 / fps)) single_convert = None
keyframe.frame_time = frame_num / fps # Can't proxy to _process_fcurves because it only supports linear interoplation.
if fkey.interpolation == "BEZIER": return self._process_keyframes([fcurve], 1, [0.0], single_convert)
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) def _santize_converted_values(self, num_channels : int, raw_values : Union[Dict, Sequence], convert : Callable):
bezier = True assert convert is not None
if isinstance(raw_values, Dict):
values = convert(**raw_values)
elif isinstance(raw_values, Sequence):
values = convert(raw_values)
else: else:
keyframe.in_tan = 0.0 raise AssertionError("Unexpected type for raw_values: {}".format(raw_values.__class__))
keyframe.out_tan = 0.0
keyframe.value = value if convert is None else convert(value) if not isinstance(values, Sequence) and isinstance(values, Iterable):
keyframes[frame_num] = keyframe values = tuple(values)
final_keyframes = [keyframes[i] for i in sorted(keyframes)] if not isinstance(values, Sequence):
return (final_keyframes, bezier) assert num_channels == 1, "Converter returned 1 value but expected {}".format(num_channels)
values = (values,)
def _process_fcurves(self, fcurves, convert, defaults=None): else:
"""Processes FCurves of different data sets and converts them into a single list of keyframes. assert len(values) == num_channels, "Converter returned {} values but expected {}".format(len(values), num_channels)
This should be used when multiple Blender fields map to a single Plasma option.""" return values
class KeyFrameData:
def __init__(self): def _process_fcurves(self, fcurves : Sequence, channels : Dict[str, int], result_channels : int,
self.values = {} convert : Callable, defaults : Dict[str, Union[float, Sequence]]) -> Sequence:
fps = self._bl_fps """This consumes a sequence of Blender FCurves that map to a single Plasma controller.
pi = math.pi Like `_process_keyframes()`, except the converter function is mandatory, and each
Blender `data_path` must have a fixed number of channels.
# 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 = {} # TODO: This fxn should probably issue a warning if any keyframes use bezier interpolation.
for fcurve in fcurves: # But there's no indication given by any other fxn when an invalid interpolation mode is
if fcurve is None: # given, so what can you do?
continue keyframe_data = type("KeyFrameData", (), {})
fps, pi = self._bl_fps, math.pi
grouped_fcurves = defaultdict(dict)
for fcurve in (i for i in fcurves if i is not None):
fcurve.update() fcurve.update()
if fcurve.data_path in grouped_fcurves:
grouped_fcurves[fcurve.data_path][fcurve.array_index] = fcurve grouped_fcurves[fcurve.data_path][fcurve.array_index] = fcurve
else:
grouped_fcurves[fcurve.data_path] = { fcurve.array_index: fcurve }
# Default values for channels that are not animated fcurve_keyframes = defaultdict(functools.partial(defaultdict, dict))
for key, value in defaults.items(): for fcurve in (i for i in fcurves if i is not None):
if key not in grouped_fcurves: for fkey in fcurve.keyframe_points:
if hasattr(value, "__len__"): fcurve_keyframes[fkey.co[0]][fcurve.data_path][fcurve.array_index] = fkey
grouped_fcurves[key] = value
else:
grouped_fcurves[key] = [value,]
# Assemble a dict { PlasmaFrameNum: { FCurveDataPath: KeyFrame } } def iter_channel_values(frame_num : int, fcurves : Dict, fkeys : Dict, num_channels : int, defaults : Union[float, Sequence]):
keyframe_points = {} for i in range(num_channels):
for fcurve in fcurves: fkey = fkeys.get(i, None)
if fkey is None:
fcurve = fcurves.get(i, None)
if fcurve is None: if fcurve is None:
continue # We would like to test this to see if it makes sense, but Blender's mathutils
for keyframe in fcurve.keyframe_points: # types don't actually implement the sequence protocol. So, we'll have to
frame_num_blender, value = keyframe.co # just try to subscript it and see what happens.
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: try:
fcurve = grouped_fcurves[chan][i] yield defaults[i]
except: except:
chan_keyframes.values[i] = defaults[chan] assert num_channels == 1, "Got a non-subscriptable default for a multi-channel keyframe."
else: yield defaults
if isinstance(fcurve, bpy.types.FCurve):
chan_keyframes.values[i] = fcurve.evaluate(chan_keyframes.frame_num_blender)
else: else:
# it's actually a default value! yield fcurve.evaluate(frame_num)
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: else:
final_keyframe.in_tan = 0.0 yield fkey.co[1]
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
def _process_keyframes(self, fcurves, convert=None): return self._sort_and_dedupe_keyframes(keyframes)
def _process_keyframes(self, fcurves, num_channels : int, default_values : Sequence, convert=None) -> Tuple[Sequence, AbstractSet]:
"""Groups all FCurves for the same frame together""" """Groups all FCurves for the same frame together"""
keyframe_data = type("KeyFrameData", (), {}) keyframe_data = type("KeyFrameData", (), {})
fps = self._bl_fps fps, pi = self._bl_fps, math.pi
pi = math.pi
keyframes = {} keyframes, fcurve_keyframes = {}, defaultdict(dict)
bez_chans = set()
for fcurve in fcurves: indexed_fcurves = { fcurve.array_index: fcurve for fcurve in fcurves if fcurve is not None }
for i, fcurve in indexed_fcurves.items():
fcurve.update() fcurve.update()
for fkey in fcurve.keyframe_points: for fkey in fcurve.keyframe_points:
frame_num, value = fkey.co fcurve_keyframes[fkey.co[0]][i] = fkey
keyframe = keyframes.get(frame_num, None)
if keyframe is None: 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:
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() keyframe = keyframe_data()
if fps == 30.0:
# hope you don't have a frame 29.9 and frame 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 = int(frame_num * (30.0 / fps))
keyframe.frame_num_blender = frame_num keyframe.frame_num_blender = frame_num
keyframe.frame_time = frame_num / fps keyframe.frame_time = frame_num / fps
keyframe.in_tans = {} keyframe.in_tans = [0.0] * num_channels
keyframe.out_tans = {} keyframe.out_tans = [0.0] * num_channels
keyframe.values = {} keyframe.values_raw = tuple(iter_values(frame_num, fkeys))
keyframes[frame_num] = keyframe if convert is None:
idx = fcurve.array_index keyframe.values = keyframe.values_raw
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)
else: else:
keyframe.in_tans[idx] = 0.0 keyframe.values = self._santize_converted_values(num_channels, keyframe.values_raw, convert)
keyframe.out_tans[idx] = 0.0
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 # Return the keyframes in a sequence sorted by frame number
final_keyframes = [keyframes[i] for i in sorted(keyframes)] return (self._sort_and_dedupe_keyframes(keyframes), bez_chans)
return (final_keyframes, bez_chans)
@property @property
def _mgr(self): def _mgr(self):

3
korman/exporter/camera.py

@ -206,6 +206,9 @@ class CameraConverter:
f1, f2 = fcurve.evaluate(begin), fcurve.evaluate(end) f1, f2 = fcurve.evaluate(begin), fcurve.evaluate(end)
if abs(f1 - f2) > 0.001: if abs(f1 - f2) > 0.001:
break 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: else:
# The animation is a loop # The animation is a loop
path.flags |= plAnimPath.kWrap path.flags |= plAnimPath.kWrap

31
korman/exporter/convert.py

@ -60,6 +60,7 @@ class Exporter:
# Step 0.8: Init the progress mgr # Step 0.8: Init the progress mgr
self.mesh.add_progress_presteps(self.report) self.mesh.add_progress_presteps(self.report)
self.report.progress_add_step("Collecting Objects") self.report.progress_add_step("Collecting Objects")
self.report.progress_add_step("Verify Competence")
self.report.progress_add_step("Harvesting Actors") self.report.progress_add_step("Harvesting Actors")
if self._op.lighting_method != "skip": if self._op.lighting_method != "skip":
etlight.LightBaker.add_progress_steps(self.report) etlight.LightBaker.add_progress_steps(self.report)
@ -81,6 +82,10 @@ class Exporter:
# us to export (both in the Age and Object Properties)... fun # us to export (both in the Age and Object Properties)... fun
self._collect_objects() 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 # 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 # that the artist made requires something to have a CoordinateInterface
self._harvest_actors() self._harvest_actors()
@ -169,6 +174,20 @@ class Exporter:
inc_progress() inc_progress()
error.raise_if_error() 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): def _export_age_info(self):
# Make life slightly easier... # Make life slightly easier...
age_info = bpy.context.scene.world.plasma_age age_info = bpy.context.scene.world.plasma_age
@ -268,7 +287,15 @@ class Exporter:
def _export_mesh_blobj(self, so, bo): def _export_mesh_blobj(self, so, bo):
self.animation.convert_object_animations(bo, so) self.animation.convert_object_animations(bo, so)
if bo.data.materials: 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: else:
self.report.msg("No material(s) on the ObData, so no drawables", indent=1) self.report.msg("No material(s) on the ObData, so no drawables", indent=1)
@ -407,7 +434,7 @@ class Exporter:
if not valid_path: if not valid_path:
filepath = bpy.context.blend_data.filepath filepath = bpy.context.blend_data.filepath
if not filepath: if not filepath:
filepath = self.filepath filepath = self._op.filepath
filepath = str(Path(filepath).with_suffix(".ktc")) filepath = str(Path(filepath).with_suffix(".ktc"))
age.texcache_path = filepath age.texcache_path = filepath
return filepath return filepath

5
korman/exporter/etlight.py

@ -375,11 +375,8 @@ class LightBaker(_MeshManager):
if not self._generate_lightgroup(bo, user_lg): if not self._generate_lightgroup(bo, user_lg):
return False 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") autocolor = vcols.get("autocolor")
if autocolor is not None: if autocolor is None:
vcols.remove(autocolor)
autocolor = vcols.new("autocolor") autocolor = vcols.new("autocolor")
toggle.track(vcols, "active", autocolor) toggle.track(vcols, "active", autocolor)

20
korman/exporter/manager.py

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

122
korman/exporter/material.py

@ -16,8 +16,10 @@
import bpy import bpy
import functools import functools
import math import math
import mathutils
from pathlib import Path from pathlib import Path
from PyHSPlasma import * from PyHSPlasma import *
from typing import Union
import weakref import weakref
from .explosions import * from .explosions import *
@ -143,7 +145,10 @@ class MaterialConverter:
"NONE": self._export_texture_type_none, "NONE": self._export_texture_type_none,
} }
self._animation_exporters = { self._animation_exporters = {
"ambientCtl": functools.partial(self._export_layer_diffuse_animation, converter=self.get_material_ambient),
"opacityCtl": self._export_layer_opacity_animation, "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, "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) self._report.msg("Exporting Material '{}' as single user '{}'", bm.name, mat_name, indent=1)
hgmat = None hgmat = None
else: 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) self._report.msg("Exporting Material '{}'", mat_name, indent=1)
hsgmat = self._mgr.find_key(hsGMaterial, name=mat_name, bl=bo) hsgmat = self._mgr.find_key(hsGMaterial, name=mat_name, bl=bo)
if hsgmat is not None: if hsgmat is not None:
@ -222,7 +229,8 @@ class MaterialConverter:
if slot.use_stencil: if slot.use_stencil:
stencils.append((idx, slot)) stencils.append((idx, slot))
else: 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: if restart_pass_next:
tex_layer.state.miscFlags |= hsGMatState.kMiscRestartPassHere tex_layer.state.miscFlags |= hsGMatState.kMiscRestartPassHere
restart_pass_next = False restart_pass_next = False
@ -249,8 +257,9 @@ class MaterialConverter:
# Plasma makes several assumptions that every hsGMaterial has at least one layer. If this # 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 # material had no Textures, we will need to initialize a default layer
if not hsgmat.layers: 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) self._propagate_material_settings(bo, bm, layer)
layer = self._export_layer_animations(bo, bm, None, 0, layer)
hsgmat.addLayer(layer.key) hsgmat.addLayer(layer.key)
# Cache this material for later # Cache this material for later
@ -349,7 +358,7 @@ class MaterialConverter:
return hsgmat.key return hsgmat.key
def export_bumpmap_slot(self, bo, bm, hsgmat, slot, idx): 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) 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 # 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) layer = self._export_layer_animations(bo, bm, slot, idx, layer)
return 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""" """Exports animations on this texture and chains the Plasma layers as needed"""
def harvest_fcurves(bl_id, collection, data_path=None): def harvest_fcurves(bl_id, collection, data_path=None):
@ -505,9 +514,16 @@ class MaterialConverter:
return None return None
fcurves = [] fcurves = []
texture = tex_slot.texture
mat_action = harvest_fcurves(bm, fcurves, "texture_slots[{}]".format(idx)) # Base layers get all of the fcurves for animating things like the diffuse color
tex_action = harvest_fcurves(texture, fcurves) 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: if not fcurves:
return base_layer return base_layer
@ -515,10 +531,9 @@ class MaterialConverter:
# and chain this biotch up as best we can. # and chain this biotch up as best we can.
layer_animation = None layer_animation = None
for attr, converter in self._animation_exporters.items(): 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 ctrl is not None:
if layer_animation is None: if layer_animation is None:
name = "{}_LayerAnim".format(base_layer.key.name)
layer_animation = self.get_texture_animation_key(bo, bm, texture).object layer_animation = self.get_texture_animation_key(bo, bm, texture).object
setattr(layer_animation, attr, ctrl) setattr(layer_animation, attr, ctrl)
@ -531,13 +546,12 @@ class MaterialConverter:
atc = layer_animation.timeConvert atc = layer_animation.timeConvert
# Since we are harvesting from the material action but are exporting to a layer, the # 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. # action's range is relatively useless. We'll figure our own. Reminder: the blender
start, end = functools.reduce(lambda x, y: (min(x[0], y[0]), max(x[1], y[1])), # documentation is wrong -- FCurve.range() returns a sequence of frame numbers, not times.
(fcurve.range() for fcurve in fcurves)) 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
atc.begin = start / fps
atc.end = end / fps
if tex_slot is not None:
layer_props = tex_slot.texture.plasma_layer layer_props = tex_slot.texture.plasma_layer
if not layer_props.anim_auto_start: if not layer_props.anim_auto_start:
atc.flags |= plAnimTimeConvert.kStopped atc.flags |= plAnimTimeConvert.kStopped
@ -547,12 +561,29 @@ class MaterialConverter:
atc.loopEnd = atc.end atc.loopEnd = atc.end
if layer_props.anim_sdl_var: if layer_props.anim_sdl_var:
layer_animation.varName = 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
return layer_animation return layer_animation
# Well, we had some FCurves but they were garbage... Too bad. # Well, we had some FCurves but they were garbage... Too bad.
return base_layer 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: for i in fcurves:
if i.data_path == "plasma_layer.opacity": if i.data_path == "plasma_layer.opacity":
base_layer.state.blendFlags |= hsGMatState.kBlendAlpha base_layer.state.blendFlags |= hsGMatState.kBlendAlpha
@ -560,7 +591,8 @@ class MaterialConverter:
return ctrl return ctrl
return None return None
def _export_layer_transform_animation(self, tex_slot, base_layer, fcurves): 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() path = tex_slot.path_from_id()
pos_path = "{}.offset".format(path) pos_path = "{}.offset".format(path)
scale_path = "{}.scale".format(path) scale_path = "{}.scale".format(path)
@ -568,6 +600,7 @@ class MaterialConverter:
# Plasma uses the controller to generate a matrix44... so we have to produce a leaf controller # 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) ctrl = self._exporter().animation.make_matrix44_controller(fcurves, pos_path, scale_path, tex_slot.offset, tex_slot.scale)
return ctrl return ctrl
return None
def _export_texture_type_environment_map(self, bo, layer, slot): def _export_texture_type_environment_map(self, bo, layer, slot):
"""Exports a Blender EnvironmentMapTexture to a plLayer""" """Exports a Blender EnvironmentMapTexture to a plLayer"""
@ -1163,22 +1196,47 @@ class MaterialConverter:
def get_bump_layer(self, bo): def get_bump_layer(self, bo):
return self._bump_mats.get(bo, None) 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): def get_texture_animation_key(self, bo, bm, texture):
"""Finds or creates the appropriate key for sending messages to an animated 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": if bo.type == "LAMP":
assert bm is None assert bm is None
bm_name = bo.name bm_name = bo.name
else: else:
assert bm is not None assert bm is not None
bm_name = bm.name 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)) raise ExportError("Texture '{}' not used in Material '{}'".format(bm_name, tex_name))
name = "{}_{}_LayerAnim".format(bm_name, tex_name) name = "{}_{}_LayerAnim".format(bm_name, tex_name)
layer = texture.plasma_layer pClass = plLayerSDLAnimation if texture is not None and texture.plasma_layer.anim_sdl_var else plLayerAnimation
pClass = plLayerSDLAnimation if layer.anim_sdl_var else plLayerAnimation
return self._mgr.find_create_key(pClass, bl=bo, name=name) return self._mgr.find_create_key(pClass, bl=bo, name=name)
@property @property
@ -1204,23 +1262,17 @@ class MaterialConverter:
if bm.use_shadeless: if bm.use_shadeless:
state.shadeFlags |= hsGMatState.kShadeWhite state.shadeFlags |= hsGMatState.kShadeWhite
if bm.emit:
state.shadeFlags |= hsGMatState.kShadeEmissive
# Colors # Colors
layer.ambient = utils.color(bpy.context.scene.world.ambient_color) layer.ambient = self.get_material_ambient(bo, bm)
layer.preshade = utils.color(bm.diffuse_color) layer.preshade = self.get_material_preshade(bo, bm)
layer.runtime = utils.color(bm.diffuse_color) layer.runtime = self.get_material_runtime(bo, bm)
layer.specular = utils.color(bm.specular_color) layer.specular = utils.color(bm.specular_color)
layer.specularPower = min(100.0, float(bm.specular_hardness)) layer.specularPower = min(100.0, float(bm.specular_hardness))
layer.LODBias = -1.0 # Seems to be the Plasma default layer.LODBias = -1.0
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)
def _requires_single_user(self, bo, bm): def _requires_single_user(self, bo, bm):
if bo.data.show_double_sided: 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/>. # along with Korman. If not, see <http://www.gnu.org/licenses/>.
import bpy import bpy
import itertools
from PyHSPlasma import * from PyHSPlasma import *
from math import fabs from math import fabs
import weakref import weakref
@ -29,6 +30,29 @@ _WARN_VERTS_PER_SPAN = 0x8000
_VERTEX_COLOR_LAYERS = {"col", "color", "colour"} _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: class _RenderLevel:
MAJOR_OPAQUE = 0 MAJOR_OPAQUE = 0
MAJOR_FRAMEBUF = 1 MAJOR_FRAMEBUF = 1
@ -39,17 +63,13 @@ class _RenderLevel:
_MAJOR_SHIFT = 28 _MAJOR_SHIFT = 28
_MINOR_MASK = ((1 << _MAJOR_SHIFT) - 1) _MINOR_MASK = ((1 << _MAJOR_SHIFT) - 1)
def __init__(self, bo, hsgmat, pass_index, blend_span=False): def __init__(self, bo, pass_index, blend_span=False):
self.level = 0 if blend_span:
if pass_index > 0: self.level = self._determine_level(bo, blend_span)
self.major = self.MAJOR_FRAMEBUF
self.minor = pass_index * 4
else: else:
self.major = self.MAJOR_BLEND if blend_span else self.MAJOR_OPAQUE self.level = 0
# Gulp... Hope you know what you're doing...
# We use the blender material's pass index (which we stashed in the hsGMaterial) to increment self.minor += pass_index * 4
# the render pass, just like it says...
self.level += pass_index
def __eq__(self, other): def __eq__(self, other):
return self.level == other.level return self.level == other.level
@ -60,19 +80,42 @@ class _RenderLevel:
def _get_major(self): def _get_major(self):
return self.level >> self._MAJOR_SHIFT return self.level >> self._MAJOR_SHIFT
def _set_major(self, value): 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) major = property(_get_major, _set_major)
def _get_minor(self): def _get_minor(self):
return self.level & self._MINOR_MASK return self.level & self._MINOR_MASK
def _set_minor(self, value): 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) 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: class _DrawableCriteria:
def __init__(self, bo, hsgmat, pass_index): def __init__(self, bo, geospan, pass_index):
self.blend_span = bool(hsgmat.layers[0].object.state.blendFlags & hsGMatState.kBlendMask) self.blend_span = bool(geospan.props & plGeometrySpan.kRequiresBlending)
self.criteria = 0 self.criteria = 0
if self.blend_span: if self.blend_span:
@ -80,7 +123,7 @@ class _DrawableCriteria:
self.criteria |= plDrawable.kCritSortFaces self.criteria |= plDrawable.kCritSortFaces
if self._span_sort_allowed(bo): if self._span_sort_allowed(bo):
self.criteria |= plDrawable.kCritSortSpans 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): def __eq__(self, other):
if not isinstance(other, _DrawableCriteria): if not isinstance(other, _DrawableCriteria):
@ -96,12 +139,12 @@ class _DrawableCriteria:
def _face_sort_allowed(self, bo): def _face_sort_allowed(self, bo):
# For now, only test the modifiers # For now, only test the modifiers
# This will need to be tweaked further for GUIs... # 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): def _span_sort_allowed(self, bo):
# For now, only test the modifiers # For now, only test the modifiers
# This will need to be tweaked further for GUIs... # 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 @property
def span_type(self): def span_type(self):
@ -118,7 +161,6 @@ class _GeoData:
self.vertices = [] self.vertices = []
class _MeshManager: class _MeshManager:
def __init__(self, report=None): def __init__(self, report=None):
if report is not None: if report is not None:
@ -174,11 +216,12 @@ class _MeshManager:
trash_mesh, bo.data = bo.data, data_meshes.get(override["mesh"]) trash_mesh, bo.data = bo.data, data_meshes.get(override["mesh"])
data_meshes.remove(trash_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"]: for cached_mod in override["modifiers"]:
mod = bo.modifiers.new(cached_mod["name"], cached_mod["type"]) mod = bo.modifiers.new(cached_mod["name"], cached_mod["type"])
for key, value in cached_mod.items(): 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 continue
setattr(mod, key, value) setattr(mod, key, value)
@ -213,7 +256,49 @@ class MeshConverter(_MeshManager):
return (num_user_texs, total_texs, max_user_texs) 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""" """Initializes a plGeometrySpan from a Blender Object and an hsGMaterial"""
geospan = plGeometrySpan() geospan = plGeometrySpan()
geospan.material = hsgmatKey geospan.material = hsgmatKey
@ -225,10 +310,22 @@ class MeshConverter(_MeshManager):
raise explosions.TooManyUVChannelsError(bo, bm, user_uvws, max_user_uvws) raise explosions.TooManyUVChannelsError(bo, bm, user_uvws, max_user_uvws)
geospan.format = total_uvws geospan.format = total_uvws
# Begin total guesswork WRT flags def is_alpha_blended(layer):
mods = bo.plasma_modifiers if layer.state.blendFlags & hsGMatState.kBlendMask:
if mods.lightmap.enabled: 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 geospan.props |= plGeometrySpan.kLiteVtxNonPreshaded
if (geospan.props & plGeometrySpan.kLiteMask) != plGeometrySpan.kLiteMaterial:
geospan.props |= plGeometrySpan.kDiffuseFoldedIn
mods = bo.plasma_modifiers
if mods.lighting.rt_lights: if mods.lighting.rt_lights:
geospan.props |= plGeometrySpan.kPropRunTimeLight geospan.props |= plGeometrySpan.kPropRunTimeLight
if not bm.use_shadows: if not bm.use_shadows:
@ -270,7 +367,7 @@ class MeshConverter(_MeshManager):
dspan.composeGeometry(True, True) dspan.composeGeometry(True, True)
inc_progress() 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. # Recall that materials is a mapping of exported materials to blender material indices.
# Therefore, geodata maps blender material indices to working geometry data. # Therefore, geodata maps blender material indices to working geometry data.
# Maybe the logic is a bit inverted, but it keeps the inner loop simple. # 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... # Locate relevant vertex color layers now...
lm = bo.plasma_modifiers.lightmap lm = bo.plasma_modifiers.lightmap
color, alpha = None, None color = None if lm.bake_lightmap else self._find_vtx_color_layer(mesh.tessface_vertex_colors)
for vcol_layer in mesh.tessface_vertex_colors: alpha = self._find_vtx_alpha_layer(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
# Convert Blender faces into things we can stuff into libHSPlasma # Convert Blender faces into things we can stuff into libHSPlasma
for i, tessface in enumerate(mesh.tessfaces): for i, tessface in enumerate(mesh.tessfaces):
@ -317,10 +407,8 @@ class MeshConverter(_MeshManager):
else: else:
src = alpha[i] src = alpha[i]
# average color becomes the alpha value # average color becomes the alpha value
tessface_alphas = (((src.color1[0] + src.color1[1] + src.color1[2]) / 3), tessface_alphas = ((sum(src.color1) / 3), (sum(src.color2) / 3),
((src.color2[0] + src.color2[1] + src.color2[2]) / 3), (sum(src.color3) / 3), (sum(src.color4) / 3))
((src.color3[0] + src.color3[1] + src.color3[2]) / 3),
((src.color4[0] + src.color4[1] + src.color4[2]) / 3))
if bumpmap is not None: if bumpmap is not None:
gradPass = [] gradPass = []
@ -350,9 +438,16 @@ class MeshConverter(_MeshManager):
for j, vertex in enumerate(tessface.vertices): for j, vertex in enumerate(tessface.vertices):
uvws = tuple([uvw[j] for uvw in tessface_uvws]) uvws = tuple([uvw[j] for uvw in tessface_uvws])
# Grab VCols # Calculate vertex colors.
vertex_color = (int(tessface_colors[j][0] * 255), int(tessface_colors[j][1] * 255), if mat2span_LUT:
int(tessface_colors[j][2] * 255), int(tessface_alphas[j] * 255)) 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 :( # 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 # 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... # Time to finish it up...
for i, data in enumerate(geodata.values()): for i, data in enumerate(geodata.values()):
geospan = geospans[i][0] geospan = geospans[i].geospan
numVerts = len(data.vertices) numVerts = len(data.vertices)
numUVs = geospan.format & plGeometrySpan.kUVCountMask numUVs = geospan.format & plGeometrySpan.kUVCountMask
@ -480,7 +575,7 @@ class MeshConverter(_MeshManager):
# Sequence of tuples (material_index, material) # Sequence of tuples (material_index, material)
return sorted(((i, material_source[i]) for i in valid_materials), key=lambda x: x[0]) 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 # 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... # Otherwise, let's *try* to share meshes as best we can...
if bo.modifiers: if bo.modifiers:
@ -492,7 +587,7 @@ class MeshConverter(_MeshManager):
# Create the DrawInterface # Create the DrawInterface
if drawables: 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: for dspan_key, idx in drawables:
diface.addDrawable(dspan_key, idx) diface.addDrawable(dspan_key, idx)
@ -512,18 +607,18 @@ class MeshConverter(_MeshManager):
return None return None
# Step 1: Export all of the doggone materials. # 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 # 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 # Step 3: Add plGeometrySpans to the appropriate DSpan and create indices
_diindices = {} _diindices = {}
for geospan, pass_index in geospans: for i in geospans:
dspan = self._find_create_dspan(bo, geospan.material.object, pass_index) dspan = self._find_create_dspan(bo, i.geospan, i.pass_index)
self._report.msg("Exported hsGMaterial '{}' geometry into '{}'", self._report.msg("Exported hsGMaterial '{}' geometry into '{}'",
geospan.material.name, dspan.key.name, indent=1) i.geospan.material.name, dspan.key.name, indent=1)
idx = dspan.addSourceSpan(geospan) idx = dspan.addSourceSpan(i.geospan)
diidx = _diindices.setdefault(dspan, []) diidx = _diindices.setdefault(dspan, [])
diidx.append(idx) diidx.append(idx)
@ -543,22 +638,27 @@ class MeshConverter(_MeshManager):
if len(materials) > 1: if len(materials) > 1:
msg = "'{}' is a WaveSet -- only one material is supported".format(bo.name) msg = "'{}' is a WaveSet -- only one material is supported".format(bo.name)
self._exporter().report.warn(msg, indent=1) self._exporter().report.warn(msg, indent=1)
matKey = self.material.export_waveset_material(bo, materials[0][1]) blmat = materials[0][1]
geospan = self._create_geospan(bo, mesh, materials[0][1], matKey) matKey = self.material.export_waveset_material(bo, blmat)
geospan = self._create_geospan(bo, mesh, None, blmat, matKey)
# FIXME: Can some of this be generalized? # FIXME: Can some of this be generalized?
geospan.props |= (plGeometrySpan.kWaterHeight | plGeometrySpan.kLiteVtxNonPreshaded | geospan.props |= (plGeometrySpan.kWaterHeight | plGeometrySpan.kLiteVtxNonPreshaded |
plGeometrySpan.kPropReverseSort | plGeometrySpan.kPropNoShadow) plGeometrySpan.kPropReverseSort | plGeometrySpan.kPropNoShadow)
geospan.waterHeight = bo.location[2] geospan.waterHeight = bo.location[2]
return [(geospan, 0)] return [_GeoSpan(bo, blmat, geospan)], None
else: else:
geospans = [None] * len(materials) 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) matKey = self.material.export_material(bo, blmat)
geospans[i] = (self._create_geospan(bo, mesh, blmat, matKey), blmat.pass_index) geospans[i] = _GeoSpan(bo, blmat,
return geospans 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) location = self._mgr.get_location(bo)
if location not in self._dspans: if location not in self._dspans:
self._dspans[location] = {} self._dspans[location] = {}
@ -569,7 +669,7 @@ class MeshConverter(_MeshManager):
# SortFaces: means we should sort the faces in this span only # 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" # 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. # 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]: if crit not in self._dspans[location]:
# AgeName_[District_]_Page_RenderLevel_Crit[Blend]Spans # AgeName_[District_]_Page_RenderLevel_Crit[Blend]Spans
@ -592,6 +692,21 @@ class MeshConverter(_MeshManager):
else: else:
return self._dspans[location][crit] 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 @property
def _mgr(self): def _mgr(self):
return self._exporter().mgr return self._exporter().mgr

3
korman/exporter/outfile.py

@ -249,7 +249,8 @@ class OutputFiles:
backing_stream = stream backing_stream = stream
# No sense in wasting time encrypting data that isn't going to be used in the export # 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) enc = kwargs.get("enc", None)
if enc is not None: if enc is not None:
stream = plEncryptedStream(self._version) stream = plEncryptedStream(self._version)

62
korman/exporter/physics.py

@ -60,9 +60,7 @@ class PhysicsConverter:
indices += (v[0], v[2], v[3],) indices += (v[0], v[2], v[3],)
return indices return indices
def _convert_mesh_data(self, bo, physical, local_space, indices=True): def _convert_mesh_data(self, bo, physical, local_space, mat, indices=True):
mat = bo.matrix_world
mesh = bo.to_mesh(bpy.context.scene, True, "RENDER", calc_tessface=False) mesh = bo.to_mesh(bpy.context.scene, True, "RENDER", calc_tessface=False)
with TemporaryObject(mesh, bpy.data.meshes.remove): with TemporaryObject(mesh, bpy.data.meshes.remove):
if local_space: if local_space:
@ -211,29 +209,41 @@ class PhysicsConverter:
if tree_xformed: if tree_xformed:
bo_xformed = bo.plasma_object.has_transform_animation 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 # MOUL: only objects that have animation data are kPhysAnim
if ver != pvMoul or bo_xformed: if ver != pvMoul or bo_xformed:
_set_phys_prop(plSimulationInterface.kPhysAnim, simIface, physical) _set_phys_prop(plSimulationInterface.kPhysAnim, simIface, physical)
# PotS: objects inheriting parent animation only are not pinned
# MOUL: animated objects in subworlds are not pinned # Any physical that is parented by not kickable (dynamic) is passive -
if bo_xformed and (ver != pvMoul or subworld is None): # meaning we don't need to report back any changes from physics. Same for
_set_phys_prop(plSimulationInterface.kPinned, simIface, physical) # plFilterCoordInterface, which filters out some axes.
# MOUL: child objects are kPassive if (bo.parent is not None and not mod.dynamic) or bo.plasma_object.ci_type == plFilterCoordInterface:
if ver == pvMoul and bo.parent is not None:
_set_phys_prop(plSimulationInterface.kPassive, simIface, physical)
# FilterCoordinateInterfaces are kPassive
if bo.plasma_object.ci_type == plFilterCoordInterface:
_set_phys_prop(plSimulationInterface.kPassive, simIface, physical) _set_phys_prop(plSimulationInterface.kPassive, simIface, physical)
# If the mass is zero, then we will fail to animate. Fix that. # If the mass is zero, then we will fail to animate. Fix that.
if physical.mass == 0.0: if physical.mass == 0.0:
physical.mass = 1.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: 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: else:
local_space = self._exporter().has_coordiface(bo) local_space, mat = False, bo.matrix_world
self._bounds_converters[bounds](bo, physical, local_space) else:
raise NotImplementedError("ODE physical transform")
self._bounds_converters[bounds](bo, physical, local_space, mat)
else: else:
simIface = so.sim.object simIface = so.sim.object
physical = simIface.physical.object physical = simIface.physical.object
@ -245,14 +255,14 @@ class PhysicsConverter:
self._apply_props(simIface, physical, kwargs) 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""" """Exports box bounds based on the object"""
physical.boundsType = plSimDefs.kBoxBounds 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) 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""" """Exports convex hull bounds based on the object"""
physical.boundsType = plSimDefs.kHullBounds 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 # bake them to convex hulls. Specifically, Windows 32-bit w/PhysX 2.6. Everything else just
# needs to have us provide some friendlier data... # needs to have us provide some friendlier data...
with bmesh_from_object(bo) as mesh: with bmesh_from_object(bo) as mesh:
mat = bo.matrix_world
if local_space: if local_space:
physical.pos = hsVector3(*mat.to_translation()) physical.pos = hsVector3(*mat.to_translation())
physical.rot = utils.quaternion(mat.to_quaternion()) physical.rot = utils.quaternion(mat.to_quaternion())
@ -273,18 +282,25 @@ class PhysicsConverter:
verts = itertools.takewhile(lambda x: isinstance(x, BMVert), result["geom"]) verts = itertools.takewhile(lambda x: isinstance(x, BMVert), result["geom"])
physical.verts = [hsVector3(*i.co) for i in verts] 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""" """Exports sphere bounds based on the object"""
physical.boundsType = plSimDefs.kSphereBounds 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) 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""" """Exports an object's mesh as exact physical bounds"""
# Triangle meshes MAY optionally specify a proxy object to fetch the triangles from...
mod = bo.plasma_modifiers.collision
if mod.enabled and mod.proxy_object is not None:
physical.boundsType = plSimDefs.kProxyBounds
vertices, indices = self._convert_mesh_data(mod.proxy_object, physical, local_space, mat)
else:
physical.boundsType = plSimDefs.kExplicitBounds physical.boundsType = plSimDefs.kExplicitBounds
vertices, indices = self._convert_mesh_data(bo, physical, local_space, mat)
vertices, indices = self._convert_mesh_data(bo, physical, local_space)
physical.verts = vertices physical.verts = vertices
physical.indices = indices physical.indices = indices

17
korman/exporter/utils.py

@ -92,3 +92,20 @@ def bmesh_object(name : str):
bm.to_mesh(mesh) bm.to_mesh(mesh)
finally: finally:
bm.free() 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): def poll_camera_objects(self, value):
return value.type == "CAMERA" 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): def poll_empty_objects(self, value):
return value.type == "EMPTY" return value.type == "EMPTY"

13
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.""" """Generates valid node sockets that can be linked to a specific socket on this node."""
from .node_deprecated import PlasmaDeprecatedNode 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__(): for dest_node_cls in bpy.types.Node.__subclasses__():
if not issubclass(dest_node_cls, PlasmaNodeBase) or issubclass(dest_node_cls, PlasmaDeprecatedNode): if not issubclass(dest_node_cls, PlasmaNodeBase) or issubclass(dest_node_cls, PlasmaDeprecatedNode):
continue continue
@ -194,6 +200,13 @@ class PlasmaNodeBase:
if socket_def.get("hidden") is True: if socket_def.get("hidden") is True:
continue 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_nodes = socket_def.get("valid_link_nodes")
valid_source_sockets = socket_def.get("valid_link_sockets") 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: if valid_source_nodes is not None and self.bl_idname not in valid_source_nodes:

48
korman/nodes/node_python.py

@ -256,6 +256,10 @@ class PlasmaPythonFileNode(PlasmaVersionedNode, bpy.types.Node):
def export(self, exporter, bo, so): def export(self, exporter, bo, so):
pfm = self.get_key(exporter, so).object 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. # No need to continue if the PFM was already generated.
if pfm.filename: if pfm.filename:
return return
@ -276,7 +280,6 @@ class PlasmaPythonFileNode(PlasmaVersionedNode, bpy.types.Node):
# Handle exporting the Python Parameters # Handle exporting the Python Parameters
attrib_sockets = (i for i in self.inputs if i.is_linked) attrib_sockets = (i for i in self.inputs if i.is_linked)
for socket in attrib_sockets: for socket in attrib_sockets:
attrib = socket.attribute_type
from_node = socket.links[0].from_node from_node = socket.links[0].from_node
value = from_node.value if socket.is_simple_value else from_node.get_key(exporter, so) value = from_node.value if socket.is_simple_value else from_node.get_key(exporter, so)
@ -285,26 +288,41 @@ class PlasmaPythonFileNode(PlasmaVersionedNode, bpy.types.Node):
for i in value: for i in value:
param = plPythonParameter() param = plPythonParameter()
param.id = socket.attribute_id param.id = socket.attribute_id
param.valueType = _attrib2param[attrib] param.valueType = _attrib2param[socket.attribute_type]
param.value = i param.value = i
# Key type sanity checking... Because I trust no user.
if not socket.is_simple_value: if not socket.is_simple_value:
if i is None: self._export_key_attrib(exporter, bo, so, i, socket)
msg = "'{}' Node '{}' didn't return a key and therefore will be unavailable to Python".format( pfm.addParameter(param)
self.id_data.name, from_node.name)
exporter.report.warn(msg, indent=3) def _export_ancillary_sceneobject(self, exporter, so : plSceneObject) -> None:
else: # Danger: Special case evil ahoy...
key_type = _attrib_key_types[attrib] # 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): if isinstance(key_type, tuple):
good_key = i.type in key_type good_key = key.type in key_type
else: else:
good_key = i.type == key_type good_key = key.type == key_type
if not good_key: if not good_key:
msg = "'{}' Node '{}' returned an unexpected key type '{}'".format( exporter.report.warn("'{}' Node '{}' returned an unexpected key type '{}'",
self.id_data.name, from_node.name, plFactory.ClassName(i.type)) self.id_data.name, socket.links[0].from_node.name,
exporter.report.warn(msg, indent=3) plFactory.ClassName(key.type), indent=3)
pfm.addParameter(param)
if isinstance(key.object, plSceneObject):
self._export_ancillary_sceneobject(exporter, key.object)
def _get_attrib_sockets(self, idx): def _get_attrib_sockets(self, idx):
for i in self.inputs: for i in self.inputs:

14
korman/nodes/node_responder.py

@ -291,7 +291,7 @@ class PlasmaResponderStateNode(PlasmaNodeBase, bpy.types.Node):
# Convert the commands # Convert the commands
commands = CommandMgr(stateMgr.responder) 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 # 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... # namely because it's impossible to wait on a command that doesn't exist...
self._generate_command(exporter, so, stateMgr.responder, commands, i) self._generate_command(exporter, so, stateMgr.responder, commands, i)
@ -340,16 +340,24 @@ class PlasmaResponderStateNode(PlasmaNodeBase, bpy.types.Node):
if msgNode.has_callbacks: if msgNode.has_callbacks:
commandMgr.add_waitable_node(msgNode) commandMgr.add_waitable_node(msgNode)
if msgNode.find_output("msgs"): if msgNode.has_linked_callbacks:
childWaitOn = commandMgr.add_wait(idx) childWaitOn = commandMgr.add_wait(idx)
msgNode.convert_callback_message(exporter, so, msg, responder.key, childWaitOn) msgNode.convert_callback_message(exporter, so, msg, responder.key, childWaitOn)
else: else:
childWaitOn = waitOn childWaitOn = waitOn
# Export any linked callback messages # 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) 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): class PlasmaRespStateSocket(PlasmaNodeSocketBase, bpy.types.NodeSocket):
bl_color = (0.388, 0.78, 0.388, 1.0) 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" 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): class PlasmaAddFlareOperator(PlasmaMeshOperator, bpy.types.Operator):
bl_idname = "mesh.plasma_flare_add" bl_idname = "mesh.plasma_flare_add"
@ -52,7 +48,6 @@ class PlasmaAddFlareOperator(PlasmaMeshOperator, bpy.types.Operator):
flare_material_name = bpy.props.StringProperty(name="Material", flare_material_name = bpy.props.StringProperty(name="Material",
description="A specially-crafted material to use for this flare", description="A specially-crafted material to use for this flare",
default=FLARE_MATERIAL_BASE_NAME, default=FLARE_MATERIAL_BASE_NAME,
update=store_material_selection,
options=set()) options=set())
@classmethod @classmethod

68
korman/operators/op_toolbox.py

@ -16,6 +16,7 @@
import bpy import bpy
from bpy.props import * from bpy.props import *
import pickle import pickle
import itertools
class ToolboxOperator: class ToolboxOperator:
@classmethod @classmethod
@ -172,6 +173,37 @@ class PlasmaToggleAllPlasmaObjectsOperator(ToolboxOperator, bpy.types.Operator):
return {"FINISHED"} 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): class PlasmaToggleEnvironmentMapsOperator(ToolboxOperator, bpy.types.Operator):
bl_idname = "texture.plasma_toggle_environment_maps" bl_idname = "texture.plasma_toggle_environment_maps"
bl_label = "Toggle Environment Maps" bl_label = "Toggle Environment Maps"
@ -204,3 +236,39 @@ class PlasmaTogglePlasmaObjectsOperator(ToolboxOperator, bpy.types.Operator):
for i in context.selected_objects: for i in context.selected_objects:
i.plasma_object.enabled = enable i.plasma_object.enabled = enable
return {"FINISHED"} 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)) setattr(cls, i.pl_id, bpy.props.PointerProperty(type=i))
bpy.types.Object.plasma_modifiers = bpy.props.PointerProperty(type=cls) 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): class PlasmaModifierSpec(bpy.types.PropertyGroup):
pass pass

19
korman/properties/modifiers/anim.py

@ -60,15 +60,30 @@ class PlasmaAnimationModifier(ActionModifier, PlasmaModifierProperties):
description="Marker indicating where the default loop begins") description="Marker indicating where the default loop begins")
loop_end = StringProperty(name="Loop End", loop_end = StringProperty(name="Loop End",
description="Marker indicating where the default loop ends") 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): def export(self, exporter, bo, so):
action = self.blender_action action = self.blender_action
anim_mod = bo.plasma_modifiers.animation
atcanim = exporter.mgr.find_create_object(plATCAnim, so=so) # 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.autoStart = self.auto_start
atcanim.loop = self.loop atcanim.loop = self.loop
# Simple start and loop info # Simple start and loop info for ATC
if action is not None: if action is not None:
markers = action.pose_markers markers = action.pose_markers
initial_marker = markers.get(self.initial_marker) initial_marker = markers.get(self.initial_marker)

20
korman/properties/modifiers/base.py

@ -30,10 +30,30 @@ class PlasmaModifierProperties(bpy.types.PropertyGroup):
def destroyed(self): def destroyed(self):
pass 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 @property
def enabled(self): def enabled(self):
return self.display_order >= 0 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): def harvest_actors(self):
return () return ()

4
korman/properties/modifiers/gui.py

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

6
korman/properties/modifiers/physics.py

@ -19,6 +19,7 @@ from PyHSPlasma import *
from .base import PlasmaModifierProperties from .base import PlasmaModifierProperties
from ...exporter import ExportError from ...exporter import ExportError
from ... import idprops
# These are the kinds of physical bounds Plasma can work with. # These are the kinds of physical bounds Plasma can work with.
# This sequence is acceptable in any EnumProperty # 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) 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) 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): def export(self, exporter, bo, so):
# All modifier properties are examined by this little stinker... # All modifier properties are examined by this little stinker...
exporter.physics.generate_physical(bo, so) 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 ...exporter.explosions import ExportError
from ... import idprops 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): class PlasmaDecalManagerRef(bpy.types.PropertyGroup):
enabled = BoolProperty(name="Enabled", enabled = BoolProperty(name="Enabled",
default=True, default=True,
@ -439,6 +531,8 @@ class PlasmaLightingMod(PlasmaModifierProperties):
return True return True
if self.id_data.plasma_object.has_transform_animation: if self.id_data.plasma_object.has_transform_animation:
return True return True
if mods.collision.enabled and mods.collision.dynamic:
return True
return False return False

8
korman/properties/modifiers/sound.py

@ -189,8 +189,12 @@ class PlasmaSound(idprops.IDPropMixin, bpy.types.PropertyGroup):
exporter.output.add_sfx(self._sound) 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 # 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 # result. Worse, PotS seems to not like static sounds that are brand-new to it. Possibly because
# streaming vs static. That's an implementation detail. # 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 pClass = plWin32StreamingSound if length > 4.0 else plWin32StaticSound
# OK. Any Plasma engine that uses OpenAL (MOUL) is subject to this restriction. # OK. Any Plasma engine that uses OpenAL (MOUL) is subject to this restriction.

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 # This is much like what happened in PyPRP
speed = self.wind_speed 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) wind_dir = hsVector3(matrix[1][0] * speed, matrix[1][1] * speed, matrix[1][2] * speed)
else: else:
# Stolen shamelessly from PyPRP # Stolen shamelessly from PyPRP

10
korman/properties/prop_object.py

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

17
korman/properties/prop_world.py

@ -31,7 +31,7 @@ class PlasmaFni(bpy.types.PropertyGroup):
fog_method = EnumProperty(name="Fog Type", fog_method = EnumProperty(name="Fog Type",
items=[ items=[
("linear", "Linear", "Linear Fog"), ("linear", "Linear", "Linear Fog"),
("exp2", "Exponential", "Exponential Fog"), ("exp", "Exponential", "Exponential Fog"),
("none", "None", "Use fog from the previous age") ("none", "None", "Use fog from the previous age")
]) ])
fog_start = FloatProperty(name="Start", fog_start = FloatProperty(name="Start",
@ -148,6 +148,14 @@ class PlasmaPage(bpy.types.PropertyGroup):
class PlasmaAge(bpy.types.PropertyGroup): class PlasmaAge(bpy.types.PropertyGroup):
def export(self, exporter): 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 = plAgeInfo()
_age_info.dayLength = self.day_length _age_info.dayLength = self.day_length
_age_info.lingerTime = 180 # this is fairly standard _age_info.lingerTime = 180 # this is fairly standard
@ -157,6 +165,10 @@ class PlasmaAge(bpy.types.PropertyGroup):
_age_info.startDateTime = self.start_time _age_info.startDateTime = self.start_time
return _age_info 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", day_length = FloatProperty(name="Day Length",
description="Length of a day (in hours) on this age", description="Length of a day (in hours) on this age",
default=30.230000, default=30.230000,
@ -169,7 +181,10 @@ class PlasmaAge(bpy.types.PropertyGroup):
min=0) min=0)
seq_prefix = IntProperty(name="Sequence Prefix", seq_prefix = IntProperty(name="Sequence Prefix",
description="A unique numerical ID for this age", description="A unique numerical ID for this age",
min=SP_PRFIX_RANGE[0],
soft_min=0, # Negative indicates global--advanced users only soft_min=0, # Negative indicates global--advanced users only
soft_max=MOUL_PREFIX_RANGE[1],
max=SP_PRFIX_RANGE[1],
default=100) default=100)
pages = CollectionProperty(name="Pages", pages = CollectionProperty(name="Pages",
description="Registry pages for this age", description="Registry pages for this age",

4
korman/ui/modifiers/anim.py

@ -40,9 +40,11 @@ def animation(modifier, layout, context):
if action: if action:
layout.prop_search(modifier, "initial_marker", action, "pose_markers", icon="PMARKER") layout.prop_search(modifier, "initial_marker", action, "pose_markers", icon="PMARKER")
col = layout.column() 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_start", action, "pose_markers", icon="PMARKER")
col.prop_search(modifier, "loop_end", 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): def animation_filter(modifier, layout, context):
split = layout.split() split = layout.split()

5
korman/ui/modifiers/physics.py

@ -39,6 +39,11 @@ def collision(modifier, layout, context):
col.active = modifier.dynamic col.active = modifier.dynamic
col.prop(modifier, "mass") 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): def subworld_def(modifier, layout, context):
layout.prop(modifier, "sub_type") layout.prop(modifier, "sub_type")
if modifier.sub_type != "dynamicav": if modifier.sub_type != "dynamicav":

30
korman/ui/modifiers/render.py

@ -18,6 +18,36 @@ import bpy
from .. import ui_list from .. import ui_list
from ...exporter.mesh import _VERTEX_COLOR_LAYERS 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): class DecalMgrListUI(bpy.types.UIList):
def draw_item(self, context, layout, data, item, icon, active_data, active_property, index=0, flt_flag=0): def draw_item(self, context, layout, data, item, icon, active_data, active_property, index=0, flt_flag=0):
if item.name: if item.name:

17
korman/ui/ui_menus.py

@ -40,8 +40,25 @@ def build_plasma_menu(self, context):
self.layout.separator() self.layout.separator()
self.layout.menu("menu.plasma_add", icon="URL") 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(): def register():
bpy.types.INFO_MT_add.append(build_plasma_menu) bpy.types.INFO_MT_add.append(build_plasma_menu)
bpy.types.INFO_MT_help.prepend(build_plasma_help_menu)
def unregister(): def unregister():
bpy.types.INFO_MT_add.remove(build_plasma_menu) 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/>. # along with Korman. If not, see <http://www.gnu.org/licenses/>.
import bpy import bpy
import itertools
class ToolboxPanel: class ToolboxPanel:
bl_category = "Tools" bl_category = "Tools"
@ -45,11 +46,23 @@ class PlasmaToolboxPanel(ToolboxPanel, bpy.types.Panel):
col.operator("object.plasma_move_selection_to_page", icon="BOOKMARKS", text="Move to Page") 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.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.label("Textures:")
col.operator("texture.plasma_enable_all_textures", icon="TEXTURE", text="Enable All") 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", text="Enable All EnvMaps").enable = True
col.operator("texture.plasma_toggle_environment_maps", icon="IMAGE_RGB_ALPHA", text="Disable All EnvMaps").enable = False 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.label("Convert:")
col.operator("object.plasma_convert_plasma_objects", icon="OBJECT_DATA", text="Plasma Objects") 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") 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 # Age Names should really be legal Python 2.x identifiers for AgeSDLHooks
legal_identifier = korlib.is_legal_python2_identifier(age.age_name) 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 # Core settings
layout.separator() layout.separator()
@ -222,17 +224,28 @@ class PlasmaAgePanel(AgeButtonsPanel, bpy.types.Panel):
col = split.column() col = split.column()
col.label("Age Settings:") col.label("Age Settings:")
col.alert = bad_prefix
col.prop(age, "seq_prefix", text="ID") 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="") 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 # Display a hint if the identifier is illegal
if not legal_identifier: if illegal_age_name:
if korlib.is_python_keyword(age.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") layout.label(text="Ages should not be named the same as a Python keyword", icon="ERROR")
elif age.age_sdl: elif age.age_sdl:
fixed_identifier = korlib.replace_python2_identifier(age.age_name) fixed_identifier = korlib.replace_python2_identifier(age.age_name)
layout.label(text="Age's SDL will use the name '{}'".format(fixed_identifier), icon="ERROR") 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() layout.separator()
split = layout.split() split = layout.split()
@ -262,6 +275,10 @@ class PlasmaEnvironmentPanel(AgeButtonsPanel, bpy.types.Panel):
layout = self.layout layout = self.layout
fni = context.world.plasma_fni 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 # basic colors
split = layout.split() split = layout.split()
col = split.column() col = split.column()

Loading…
Cancel
Save