From cbd053154c5d938fb7049b850cc00b49dcf2ac64 Mon Sep 17 00:00:00 2001 From: Joseph Davies Date: Sun, 2 Aug 2020 20:59:39 -0700 Subject: [PATCH 01/50] Remove spurious code in lamp flare operator. --- korman/operators/op_mesh.py | 5 ----- 1 file changed, 5 deletions(-) diff --git a/korman/operators/op_mesh.py b/korman/operators/op_mesh.py index 85958bb..5a5a98e 100644 --- a/korman/operators/op_mesh.py +++ b/korman/operators/op_mesh.py @@ -28,10 +28,6 @@ class PlasmaMeshOperator: FLARE_MATERIAL_BASE_NAME = "FLAREGEN" -def store_material_selection(self, value): - if bpy.data.materials.get(value, None): - bpy.context.scene.plasma_scene.last_flare_material = value - class PlasmaAddFlareOperator(PlasmaMeshOperator, bpy.types.Operator): bl_idname = "mesh.plasma_flare_add" @@ -52,7 +48,6 @@ class PlasmaAddFlareOperator(PlasmaMeshOperator, bpy.types.Operator): flare_material_name = bpy.props.StringProperty(name="Material", description="A specially-crafted material to use for this flare", default=FLARE_MATERIAL_BASE_NAME, - update=store_material_selection, options=set()) @classmethod From 79f81fc63aad1b4744b44766a6b1a3fd012901cb Mon Sep 17 00:00:00 2001 From: Adam Johnson Date: Wed, 2 Sep 2020 10:52:21 -0400 Subject: [PATCH 02/50] Fix issue with toolbox operators clobbering page settings. This happens in ZLZ-imported Ages, probably due to object name collisions from multiple pages. --- korman/properties/prop_object.py | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/korman/properties/prop_object.py b/korman/properties/prop_object.py index e9de5e9..4413160 100644 --- a/korman/properties/prop_object.py +++ b/korman/properties/prop_object.py @@ -19,13 +19,11 @@ from PyHSPlasma import * class PlasmaObject(bpy.types.PropertyGroup): def _enabled(self, context): - # This makes me sad - if not self.is_inited: + if not self.is_property_set("page"): self._init(context) - self.is_inited = True def _init(self, context): - o = context.object + o = self.id_data age = context.scene.world.plasma_age # We want to encourage the pages = layers paradigm. @@ -47,8 +45,8 @@ class PlasmaObject(bpy.types.PropertyGroup): page = StringProperty(name="Page", description="Page this object will be exported to") - # Implementation Details - is_inited = BoolProperty(description="INTERNAL: Init proc complete", + # DEAD - leaving in just in case external code uses it + is_inited = BoolProperty(description="DEAD", default=False, options={"HIDDEN"}) From 2f925221ff628ce95f7c87f46bf72e823ba10e6c Mon Sep 17 00:00:00 2001 From: Adam Johnson Date: Thu, 3 Sep 2020 17:49:07 -0400 Subject: [PATCH 03/50] Add support for triangle mesh proxy collision. --- korman/exporter/physics.py | 11 +++++++++-- korman/properties/modifiers/physics.py | 6 ++++++ korman/ui/modifiers/physics.py | 5 +++++ 3 files changed, 20 insertions(+), 2 deletions(-) diff --git a/korman/exporter/physics.py b/korman/exporter/physics.py index d13f221..4a4aa50 100644 --- a/korman/exporter/physics.py +++ b/korman/exporter/physics.py @@ -282,9 +282,16 @@ class PhysicsConverter: def _export_trimesh(self, bo, physical, local_space): """Exports an object's mesh as exact physical bounds""" - physical.boundsType = plSimDefs.kExplicitBounds - vertices, indices = self._convert_mesh_data(bo, physical, local_space) + # 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) + else: + physical.boundsType = plSimDefs.kExplicitBounds + vertices, indices = self._convert_mesh_data(bo, physical, local_space) + physical.verts = vertices physical.indices = indices diff --git a/korman/properties/modifiers/physics.py b/korman/properties/modifiers/physics.py index f561144..907875d 100644 --- a/korman/properties/modifiers/physics.py +++ b/korman/properties/modifiers/physics.py @@ -19,6 +19,7 @@ from PyHSPlasma import * from .base import PlasmaModifierProperties from ...exporter import ExportError +from ... import idprops # These are the kinds of physical bounds Plasma can work with. # This sequence is acceptable in any EnumProperty @@ -62,6 +63,11 @@ class PlasmaCollider(PlasmaModifierProperties): mass = FloatProperty(name="Mass", description="Mass of object in pounds", min=0.0, default=1.0) start_asleep = BoolProperty(name="Start Asleep", description="Object is not active until influenced by another object", default=False) + proxy_object = PointerProperty(name="Proxy", + description="Object used as the collision geometry", + type=bpy.types.Object, + poll=idprops.poll_mesh_objects) + def export(self, exporter, bo, so): # All modifier properties are examined by this little stinker... exporter.physics.generate_physical(bo, so) diff --git a/korman/ui/modifiers/physics.py b/korman/ui/modifiers/physics.py index 615e476..fa9d8e9 100644 --- a/korman/ui/modifiers/physics.py +++ b/korman/ui/modifiers/physics.py @@ -39,6 +39,11 @@ def collision(modifier, layout, context): col.active = modifier.dynamic col.prop(modifier, "mass") + layout.separator() + row = layout.row() + row.active = modifier.bounds == "trimesh" + row.prop(modifier, "proxy_object") + def subworld_def(modifier, layout, context): layout.prop(modifier, "sub_type") if modifier.sub_type != "dynamicav": From bac5a7127f28e878e27573dd86b98e7bb67787ae Mon Sep 17 00:00:00 2001 From: James Thomas Date: Mon, 7 Sep 2020 16:23:37 -0400 Subject: [PATCH 04/50] Added conversion function for turning font objects into mesh objects that can be exported to Plasma. --- korman/exporter/convert.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/korman/exporter/convert.py b/korman/exporter/convert.py index ffcafe2..60a322c 100644 --- a/korman/exporter/convert.py +++ b/korman/exporter/convert.py @@ -272,6 +272,20 @@ class Exporter: else: self.report.msg("No material(s) on the ObData, so no drawables", indent=1) + def _export_font_blobj(self, so, bo): + self.report.msg("Converting font to mesh for export") + bpy.ops.object.select_all(action='DESELECT') + bpy.context.scene.objects.active = bo + bo.select = True + convertible = bpy.ops.object.convert.poll() + if convertible: + bpy.ops.object.convert(target='MESH', keep_original= True) + convertedFont = bpy.context.active_object + self._export_mesh_blobj(so, convertedFont) + bpy.ops.object.delete() + else: + self.report.msg("not convertible, skipping...") + def _export_referenced_node_trees(self): self.report.progress_advance() self.report.progress_range = len(self.want_node_trees) From e1b95378868205b8f379c02159e33e249ddaec57 Mon Sep 17 00:00:00 2001 From: Adam Johnson Date: Mon, 7 Sep 2020 17:29:54 -0400 Subject: [PATCH 05/50] Avoid using object operators for TextCurve conversions. --- korman/exporter/convert.py | 15 +++------------ korman/exporter/utils.py | 17 +++++++++++++++++ 2 files changed, 20 insertions(+), 12 deletions(-) diff --git a/korman/exporter/convert.py b/korman/exporter/convert.py index 60a322c..100dcb6 100644 --- a/korman/exporter/convert.py +++ b/korman/exporter/convert.py @@ -273,18 +273,9 @@ class Exporter: self.report.msg("No material(s) on the ObData, so no drawables", indent=1) def _export_font_blobj(self, so, bo): - self.report.msg("Converting font to mesh for export") - bpy.ops.object.select_all(action='DESELECT') - bpy.context.scene.objects.active = bo - bo.select = True - convertible = bpy.ops.object.convert.poll() - if convertible: - bpy.ops.object.convert(target='MESH', keep_original= True) - convertedFont = bpy.context.active_object - self._export_mesh_blobj(so, convertedFont) - bpy.ops.object.delete() - else: - self.report.msg("not convertible, skipping...") + self.animation.convert_object_animations(bo, so) + with utils.temporary_mesh_object(bo) as meshObj: + self._export_mesh_blobj(so, meshObj) def _export_referenced_node_trees(self): self.report.progress_advance() diff --git a/korman/exporter/utils.py b/korman/exporter/utils.py index 9385ecf..87d894b 100644 --- a/korman/exporter/utils.py +++ b/korman/exporter/utils.py @@ -92,3 +92,20 @@ def bmesh_object(name : str): bm.to_mesh(mesh) finally: bm.free() + +@contextmanager +def temporary_mesh_object(source : bpy.types.Object) -> bpy.types.Object: + """Creates a temporary mesh object from a nonmesh object that will only exist for the duration + of the context.""" + assert source.type != "MESH" + + obj = bpy.data.objects.new(source.name, source.to_mesh(bpy.context.scene, True, "RENDER")) + obj.draw_type = "WIRE" + obj.matrix_basis, obj.matrix_world = source.matrix_basis, source.matrix_world + obj.parent = source.parent + + bpy.context.scene.objects.link(obj) + try: + yield obj + finally: + bpy.data.objects.remove(obj) From 3cdc2c8e94d1f7dda4dd521d3880079b4971fcac Mon Sep 17 00:00:00 2001 From: Patrick Dulebohn <40325124+DoobesURU@users.noreply.github.com> Date: Mon, 14 Sep 2020 10:31:19 -0400 Subject: [PATCH 06/50] Fix Rail Camera keyframe issue Keeps Korman from exporting a rail camera with only one keyframe (or duplicate location keyframes), which causes the client to crash. --- korman/exporter/camera.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/korman/exporter/camera.py b/korman/exporter/camera.py index 164f302..4c5b8e3 100644 --- a/korman/exporter/camera.py +++ b/korman/exporter/camera.py @@ -206,6 +206,9 @@ class CameraConverter: f1, f2 = fcurve.evaluate(begin), fcurve.evaluate(end) if abs(f1 - f2) > 0.001: break + # to avoid single/duplicate keyframe client crash (per Hoikas) + if any((len(i.keys) == 1 for i in (pos_ctrl.X, pos_ctrl.Y, pos_ctrl.Z) if i is not None)): + raise ExportError("'{}': Rail Camera must have more than one keyframe", bo.name) else: # The animation is a loop path.flags |= plAnimPath.kWrap From ecf7f8207f940daed5b83d8fad25174cda146977 Mon Sep 17 00:00:00 2001 From: Darryl Pogue Date: Wed, 16 Sep 2020 22:01:24 -0700 Subject: [PATCH 07/50] Ensure alpha vcol paint makes a BlendSpan --- korman/exporter/mesh.py | 24 +++++++++++++++++------- 1 file changed, 17 insertions(+), 7 deletions(-) diff --git a/korman/exporter/mesh.py b/korman/exporter/mesh.py index 4ef35ed..cb7d388 100644 --- a/korman/exporter/mesh.py +++ b/korman/exporter/mesh.py @@ -39,7 +39,7 @@ class _RenderLevel: _MAJOR_SHIFT = 28 _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 pass_index > 0: self.major = self.MAJOR_FRAMEBUF @@ -71,8 +71,8 @@ class _RenderLevel: class _DrawableCriteria: - def __init__(self, bo, hsgmat, pass_index): - self.blend_span = bool(hsgmat.layers[0].object.state.blendFlags & hsGMatState.kBlendMask) + def __init__(self, bo, geospan, pass_index): + self.blend_span = bool(geospan.props & plGeometrySpan.kRequiresBlending) self.criteria = 0 if self.blend_span: @@ -80,7 +80,7 @@ class _DrawableCriteria: self.criteria |= plDrawable.kCritSortFaces if self._span_sort_allowed(bo): self.criteria |= plDrawable.kCritSortSpans - self.render_level = _RenderLevel(bo, hsgmat, pass_index, self.blend_span) + self.render_level = _RenderLevel(bo, pass_index, self.blend_span) def __eq__(self, other): if not isinstance(other, _DrawableCriteria): @@ -218,6 +218,10 @@ class MeshConverter(_MeshManager): geospan = plGeometrySpan() geospan.material = hsgmatKey + # Mark us as needing a BlendSpan if the material require blending + if hsgmatKey.object.layers[0].object.state.blendFlags & hsGMatState.kBlendMask: + geospan.props |= plGeometrySpan.kRequiresBlending + # GeometrySpan format # For now, we really only care about the number of UVW Channels user_uvws, total_uvws, max_user_uvws = self._calc_num_uvchans(bo, mesh) @@ -279,6 +283,7 @@ class MeshConverter(_MeshManager): # Locate relevant vertex color layers now... lm = bo.plasma_modifiers.lightmap + has_vtx_alpha = False color, alpha = None, None for vcol_layer in mesh.tessface_vertex_colors: name = vcol_layer.name.lower() @@ -353,6 +358,7 @@ class MeshConverter(_MeshManager): # Grab VCols vertex_color = (int(tessface_colors[j][0] * 255), int(tessface_colors[j][1] * 255), int(tessface_colors[j][2] * 255), int(tessface_alphas[j] * 255)) + has_vtx_alpha |= bool(tessface_alphas[j] < 1.0) # 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 @@ -430,6 +436,10 @@ class MeshConverter(_MeshManager): uvMap[numUVs - 1].normalize() vtx.uvs = uvMap + # Mark us for blending if we have a alpha vertex paint layer + if has_vtx_alpha: + geospan.props |= plGeometrySpan.kRequiresBlending + # If we're still here, let's add our data to the GeometrySpan geospan.indices = data.triangles geospan.vertices = data.vertices @@ -520,7 +530,7 @@ class MeshConverter(_MeshManager): # Step 3: Add plGeometrySpans to the appropriate DSpan and create indices _diindices = {} for geospan, pass_index in geospans: - dspan = self._find_create_dspan(bo, geospan.material.object, pass_index) + dspan = self._find_create_dspan(bo, geospan, pass_index) self._report.msg("Exported hsGMaterial '{}' geometry into '{}'", geospan.material.name, dspan.key.name, indent=1) idx = dspan.addSourceSpan(geospan) @@ -558,7 +568,7 @@ class MeshConverter(_MeshManager): geospans[i] = (self._create_geospan(bo, mesh, blmat, matKey), blmat.pass_index) return geospans - def _find_create_dspan(self, bo, hsgmat, pass_index): + def _find_create_dspan(self, bo, geospan, pass_index): location = self._mgr.get_location(bo) if location not in self._dspans: self._dspans[location] = {} @@ -569,7 +579,7 @@ class MeshConverter(_MeshManager): # SortFaces: means we should sort the faces in this span only # We're using pass index to do just what it was designed for. Cyan has a nicer "depends on" # draw component, but pass index is the Blender way, so that's what we're doing. - crit = _DrawableCriteria(bo, hsgmat, pass_index) + crit = _DrawableCriteria(bo, geospan, pass_index) if crit not in self._dspans[location]: # AgeName_[District_]_Page_RenderLevel_Crit[Blend]Spans From 16b0ebdc15752f2ba6f68e1d08b87e256160b7d2 Mon Sep 17 00:00:00 2001 From: Adam Johnson Date: Sat, 19 Sep 2020 18:16:51 -0400 Subject: [PATCH 08/50] Fix potential blender crash during export. Blender can crash during export when baking vertex colors to shared mesh data objects. Doing that can lead to lighting gotchas, but it's better to have lighting gotchas than crashes. --- korman/exporter/etlight.py | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/korman/exporter/etlight.py b/korman/exporter/etlight.py index 1f6e306..a79aba5 100644 --- a/korman/exporter/etlight.py +++ b/korman/exporter/etlight.py @@ -375,12 +375,9 @@ class LightBaker(_MeshManager): if not self._generate_lightgroup(bo, user_lg): return False - # I have heard tale of some moar "No valid image to bake to" boogs if there is a really - # old copy of the autocolor layer on the mesh. Nuke it. autocolor = vcols.get("autocolor") - if autocolor is not None: - vcols.remove(autocolor) - autocolor = vcols.new("autocolor") + if autocolor is None: + autocolor = vcols.new("autocolor") toggle.track(vcols, "active", autocolor) # Mark "autocolor" as our active render layer From a3e5af042fa49d1d03169d36ed75e14a6933d07b Mon Sep 17 00:00:00 2001 From: Adam Johnson Date: Wed, 23 Sep 2020 23:00:04 -0400 Subject: [PATCH 09/50] Fix #205. --- korman/nodes/node_responder.py | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/korman/nodes/node_responder.py b/korman/nodes/node_responder.py index a400182..10ccf69 100644 --- a/korman/nodes/node_responder.py +++ b/korman/nodes/node_responder.py @@ -291,7 +291,7 @@ class PlasmaResponderStateNode(PlasmaNodeBase, bpy.types.Node): # Convert the commands commands = CommandMgr(stateMgr.responder) - for i in self.find_outputs("msgs"): + for i in self._get_child_messages(): # slight optimization--commands attached to states can't wait on other commands # namely because it's impossible to wait on a command that doesn't exist... self._generate_command(exporter, so, stateMgr.responder, commands, i) @@ -340,16 +340,24 @@ class PlasmaResponderStateNode(PlasmaNodeBase, bpy.types.Node): if msgNode.has_callbacks: commandMgr.add_waitable_node(msgNode) - if msgNode.find_output("msgs"): + if msgNode.has_linked_callbacks: childWaitOn = commandMgr.add_wait(idx) msgNode.convert_callback_message(exporter, so, msg, responder.key, childWaitOn) else: childWaitOn = waitOn # Export any linked callback messages - for i in msgNode.find_outputs("msgs"): + for i in self._get_child_messages(msgNode): self._generate_command(exporter, so, responder, commandMgr, i, childWaitOn) + def _get_child_messages(self, node=None): + """Returns a list of the message nodes sent by `node`. The list is sorted such that any + messages with callbacks are last in the list, allowing proper wait generation. + """ + if node is None: + node = self + return sorted(node.find_outputs("msgs"), key=lambda x: x.has_callbacks and x.has_linked_callbacks) + class PlasmaRespStateSocket(PlasmaNodeSocketBase, bpy.types.NodeSocket): bl_color = (0.388, 0.78, 0.388, 1.0) From 55d3e36eac5ea88bd087420b04695313347a662e Mon Sep 17 00:00:00 2001 From: Patrick Dulebohn Date: Sun, 27 Sep 2020 05:10:40 -0400 Subject: [PATCH 10/50] Add Double Sided Operators Adds toolbar options to toggle double sided meshes --- korman/operators/op_toolbox.py | 31 +++++++++++++++++++++++++++++++ korman/ui/ui_toolbox.py | 6 ++++++ 2 files changed, 37 insertions(+) diff --git a/korman/operators/op_toolbox.py b/korman/operators/op_toolbox.py index bb07463..15ef6ae 100644 --- a/korman/operators/op_toolbox.py +++ b/korman/operators/op_toolbox.py @@ -171,6 +171,37 @@ class PlasmaToggleAllPlasmaObjectsOperator(ToolboxOperator, bpy.types.Operator): i.plasma_object.enabled = self.enable return {"FINISHED"} + +class PlasmaToggleDoubleSidedOperator(ToolboxOperator, bpy.types.Operator): + bl_idname = "mesh.plasma_toggle_double_sided" + bl_label = "Toggle All Double Sided" + bl_description = "Toggles all meshes to be double sided" + + enable = BoolProperty(name="Enable", description="Enable Double Sided") + + def execute(self, context): + enable = self.enable + for mesh in bpy.data.meshes: + mesh.show_double_sided = enable + return {"FINISHED"} + + +class PlasmaToggleDoubleSidedSelectOperator(ToolboxOperator, bpy.types.Operator): + bl_idname = "mesh.plasma_toggle_double_sided_selected" + bl_label = "Toggle Selected Double Sided" + bl_description = "Toggles selected meshes double sided value" + + @classmethod + def poll(cls, context): + return super().poll(context) and hasattr(bpy.context, "selected_objects") + + def execute(self, context): + mesh_list = [i.data for i in context.selected_objects if i.type == "MESH"] + enable = not all((mesh.show_double_sided for mesh in mesh_list)) + for mesh in mesh_list: + mesh.show_double_sided = enable + return {"FINISHED"} + class PlasmaToggleEnvironmentMapsOperator(ToolboxOperator, bpy.types.Operator): bl_idname = "texture.plasma_toggle_environment_maps" diff --git a/korman/ui/ui_toolbox.py b/korman/ui/ui_toolbox.py index 56a86da..6eecab9 100644 --- a/korman/ui/ui_toolbox.py +++ b/korman/ui/ui_toolbox.py @@ -50,6 +50,12 @@ class PlasmaToolboxPanel(ToolboxPanel, bpy.types.Panel): col.operator("texture.plasma_toggle_environment_maps", icon="IMAGE_RGB", text="Enable All EnvMaps").enable = True col.operator("texture.plasma_toggle_environment_maps", icon="IMAGE_RGB_ALPHA", text="Disable All EnvMaps").enable = False + # Double Sided Operators + col.label("Double Sided:") + col.operator("mesh.plasma_toggle_double_sided", icon="MESH_DATA", text="Disable All").enable = False + all_double_sided = all((i.data.show_double_sided for i in bpy.context.selected_objects if i.type == "MESH")) + col.operator("mesh.plasma_toggle_double_sided_selected", icon="BORDER_RECT", text="Disable Selection" if all_double_sided else "Enable Selection") + col.label("Convert:") col.operator("object.plasma_convert_plasma_objects", icon="OBJECT_DATA", text="Plasma Objects") col.operator("texture.plasma_convert_layer_opacities", icon="IMAGE_RGB_ALPHA", text="Layer Opacities") From 56c2fdd054e66059bae881dfd6763b186c4e7683 Mon Sep 17 00:00:00 2001 From: Adam Johnson Date: Mon, 28 Sep 2020 12:24:28 -0400 Subject: [PATCH 11/50] Fix issue with static sounds not playing in PotS. --- korman/properties/modifiers/sound.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/korman/properties/modifiers/sound.py b/korman/properties/modifiers/sound.py index 00f289a..71b37fd 100644 --- a/korman/properties/modifiers/sound.py +++ b/korman/properties/modifiers/sound.py @@ -189,9 +189,13 @@ class PlasmaSound(idprops.IDPropMixin, bpy.types.PropertyGroup): exporter.output.add_sfx(self._sound) # There is some bug in the MOUL code that causes a crash if this does not match the expected - # result. There's no sense in debugging that though--the user should never specify - # streaming vs static. That's an implementation detail. - pClass = plWin32StreamingSound if length > 4.0 else plWin32StaticSound + # result. Worse, PotS seems to not like static sounds that are brand-new to it. Possibly because + # it needs to be decompressed outside of game. There's no sense in debugging any of that + # though--the user should never specify streaming vs static. That's an implementation detail. + if exporter.mgr.getVer() != pvMoul and self._sound.plasma_sound.package: + pClass = plWin32StreamingSound + else: + pClass = plWin32StreamingSound if length > 4.0 else plWin32StaticSound # OK. Any Plasma engine that uses OpenAL (MOUL) is subject to this restriction. # 3D Positional audio MUST... and I mean MUST... have mono emitters. From 633b3a42344488e51015690df2a85287a991c0fd Mon Sep 17 00:00:00 2001 From: Adam Johnson Date: Mon, 28 Sep 2020 12:49:31 -0400 Subject: [PATCH 12/50] Fix #187. We were only comparing connections in one direction in order to make the list of suggestions. This ensures that suggested connections are validated in both directions. --- korman/nodes/node_core.py | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/korman/nodes/node_core.py b/korman/nodes/node_core.py index 597b2f5..86a2d3f 100644 --- a/korman/nodes/node_core.py +++ b/korman/nodes/node_core.py @@ -181,6 +181,12 @@ class PlasmaNodeBase: """Generates valid node sockets that can be linked to a specific socket on this node.""" from .node_deprecated import PlasmaDeprecatedNode + source_socket_props = getattr(self.__class__, "output_sockets", {}) if is_output else \ + getattr(self.__class__, "input_sockets", {}) + source_socket_def = source_socket_props.get(socket.alias, {}) + valid_dest_sockets = source_socket_def.get("valid_link_sockets") + valid_dest_nodes = source_socket_def.get("valid_link_nodes") + for dest_node_cls in bpy.types.Node.__subclasses__(): if not issubclass(dest_node_cls, PlasmaNodeBase) or issubclass(dest_node_cls, PlasmaDeprecatedNode): continue @@ -193,7 +199,14 @@ class PlasmaNodeBase: continue if socket_def.get("hidden") is True: continue - + + # Can this socket link to the socket_def on the destination node? + if valid_dest_nodes is not None and dest_node_cls.bl_idname not in valid_dest_nodes: + continue + if valid_dest_sockets is not None and socket_def["type"] not in valid_dest_sockets: + continue + + # Can the socket_def on the destination node link to this socket? valid_source_nodes = socket_def.get("valid_link_nodes") valid_source_sockets = socket_def.get("valid_link_sockets") if valid_source_nodes is not None and self.bl_idname not in valid_source_nodes: From 690d75b175cf99df3447cf7171efd98fd79982e9 Mon Sep 17 00:00:00 2001 From: Adam Johnson Date: Mon, 28 Sep 2020 20:52:54 -0400 Subject: [PATCH 13/50] Add scaffolding for #102. --- korman/exporter/convert.py | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/korman/exporter/convert.py b/korman/exporter/convert.py index 100dcb6..a1d15e7 100644 --- a/korman/exporter/convert.py +++ b/korman/exporter/convert.py @@ -60,6 +60,7 @@ class Exporter: # Step 0.8: Init the progress mgr self.mesh.add_progress_presteps(self.report) self.report.progress_add_step("Collecting Objects") + self.report.progress_add_step("Verify Competence") self.report.progress_add_step("Harvesting Actors") if self._op.lighting_method != "skip": etlight.LightBaker.add_progress_steps(self.report) @@ -81,6 +82,10 @@ class Exporter: # us to export (both in the Age and Object Properties)... fun self._collect_objects() + # Step 2.1: Run through all the objects we collected in Step 2 and make sure there + # is no ruddy funny business going on. + self._check_sanity() + # Step 2.5: Run through all the objects we collected in Step 2 and see if any relationships # that the artist made requires something to have a CoordinateInterface self._harvest_actors() @@ -169,6 +174,20 @@ class Exporter: inc_progress() error.raise_if_error() + def _check_sanity(self): + self.report.progress_advance() + self.report.progress_range = len(self._objects) + inc_progress = self.report.progress_increment + + self.report.msg("\nEnsuring Age is sane...") + for bl_obj in self._objects: + for mod in bl_obj.plasma_modifiers.modifiers: + fn = getattr(mod, "sanity_check", None) + if fn is not None: + fn() + inc_progress() + self.report.msg("... Age is grinning and holding a spatula. Must be OK, then.") + def _export_age_info(self): # Make life slightly easier... age_info = bpy.context.scene.world.plasma_age From b9e8b4434e8532e7c2eb4c0e66c6fb9e24da40e9 Mon Sep 17 00:00:00 2001 From: Patrick Dulebohn Date: Tue, 29 Sep 2020 15:45:45 -0400 Subject: [PATCH 14/50] Add Sound Export Operator Buttons Adds buttons to enable and disable export/packaging of sounds (WIP) --- korman/operators/op_toolbox.py | 37 ++++++++++++++++++++++++++++++++++ korman/ui/ui_toolbox.py | 7 +++++++ 2 files changed, 44 insertions(+) diff --git a/korman/operators/op_toolbox.py b/korman/operators/op_toolbox.py index 15ef6ae..a5ffb64 100644 --- a/korman/operators/op_toolbox.py +++ b/korman/operators/op_toolbox.py @@ -16,6 +16,7 @@ import bpy from bpy.props import * import pickle +import itertools class ToolboxOperator: @classmethod @@ -235,3 +236,39 @@ class PlasmaTogglePlasmaObjectsOperator(ToolboxOperator, bpy.types.Operator): for i in context.selected_objects: i.plasma_object.enabled = enable return {"FINISHED"} + + +class PlasmaToggleSoundExportOperator(ToolboxOperator, bpy.types.Operator): + bl_idname = "object.plasma_toggle_sound_export" + bl_label = "Toggle Sound Export" + bl_description = "Toggles the Export function of all sound emitters' files" + + enable = BoolProperty(name="Enable", description="Sound Export Enable") + + def execute(self, context): + enable = self.enable + for i in bpy.data.objects: + if i.plasma_modifiers.soundemit is None: + continue + for sound in i.plasma_modifiers.soundemit.sounds: + sound.package = enable + return {"FINISHED"} + + +class PlasmaToggleSoundExportSelectedOperator(ToolboxOperator, bpy.types.Operator): + bl_idname = "object.plasma_toggle_sound_export_selected" + bl_label = "Toggle Selected Sound Export" + bl_description = "Toggles the Export function of selected sound emitters' files." + + @classmethod + def poll(cls, context): + return super().poll(context) and hasattr(bpy.context, "selected_objects") + + def execute(self, context): + enable = not all((i.package for i in itertools.chain.from_iterable(i.plasma_modifiers.soundemit.sounds for i in bpy.context.selected_objects))) + for i in context.selected_objects: + if i.plasma_modifiers.soundemit is None: + continue + for sound in i.plasma_modifiers.soundemit.sounds: + sound.package = enable + return {"FINISHED"} diff --git a/korman/ui/ui_toolbox.py b/korman/ui/ui_toolbox.py index 6eecab9..b8c1539 100644 --- a/korman/ui/ui_toolbox.py +++ b/korman/ui/ui_toolbox.py @@ -14,6 +14,7 @@ # along with Korman. If not, see . import bpy +import itertools class ToolboxPanel: bl_category = "Tools" @@ -44,6 +45,12 @@ class PlasmaToolboxPanel(ToolboxPanel, bpy.types.Panel): col.label("Plasma Pages:") col.operator("object.plasma_move_selection_to_page", icon="BOOKMARKS", text="Move to Page") col.operator("object.plasma_select_page_objects", icon="RESTRICT_SELECT_OFF", text="Select Objects") + + col.label("Package Sounds:") + col.operator("object.plasma_toggle_sound_export", icon="MUTE_IPO_OFF", text="Enable All").enable = True + all_sounds_export = all((i.package for i in itertools.chain.from_iterable(i.plasma_modifiers.soundemit.sounds for i in bpy.context.selected_objects if i.plasma_modifiers.soundemit.enabled))) + col.operator("object.plasma_toggle_sound_export_selected", icon="OUTLINER_OB_SPEAKER", text="Disable Selection" if all_sounds_export else "Enable Selection") + col.operator("object.plasma_toggle_sound_export", icon="MUTE_IPO_ON", text="Disable All").enable = False col.label("Textures:") col.operator("texture.plasma_enable_all_textures", icon="TEXTURE", text="Enable All") From c8dc5df5d86c1a8c5bf88e09509d826255526b09 Mon Sep 17 00:00:00 2001 From: Joseph Davies Date: Mon, 4 Nov 2019 09:17:13 -0800 Subject: [PATCH 15/50] Add Help menu for Korman. --- korman/ui/ui_menus.py | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/korman/ui/ui_menus.py b/korman/ui/ui_menus.py index 1073a98..89f6fe0 100644 --- a/korman/ui/ui_menus.py +++ b/korman/ui/ui_menus.py @@ -40,8 +40,25 @@ def build_plasma_menu(self, context): self.layout.separator() self.layout.menu("menu.plasma_add", icon="URL") + +class PlasmaHelpMenu(PlasmaMenu, bpy.types.Menu): + bl_idname = "menu.plasma_help" + bl_label = "Korman..." + + def draw(self, context): + layout = self.layout + layout.operator("wm.url_open", text="About Korman", icon="URL").url = "https://guildofwriters.org/wiki/Korman" + layout.operator("wm.url_open", text="Getting Started", icon="URL").url = "https://guildofwriters.org/wiki/Korman:Getting_Started" + layout.operator("wm.url_open", text="Tutorials", icon="URL").url = "https://guildofwriters.org/wiki/Category:Korman_Tutorials" + + +def build_plasma_help_menu(self, context): + self.layout.menu("menu.plasma_help", text="Korman", icon="URL") + def register(): bpy.types.INFO_MT_add.append(build_plasma_menu) + bpy.types.INFO_MT_help.prepend(build_plasma_help_menu) def unregister(): bpy.types.INFO_MT_add.remove(build_plasma_menu) + bpy.types.INFO_MT_help.remove(build_plasma_help_menu) From fedb7c91f56147159710746654eeec5d95ef5e1e Mon Sep 17 00:00:00 2001 From: Adam Johnson Date: Mon, 28 Sep 2020 20:53:59 -0400 Subject: [PATCH 16/50] Add "Blending" modifier and improve render level export. This matches us up more closely with what PlasmaMAX does and allows us to really fix x-raying effects by properly forcing objects into the correct render pass. --- korman/exporter/mesh.py | 46 +++++++++---- korman/idprops.py | 3 + korman/properties/modifiers/__init__.py | 4 ++ korman/properties/modifiers/base.py | 20 ++++++ korman/properties/modifiers/render.py | 92 +++++++++++++++++++++++++ korman/ui/modifiers/render.py | 30 ++++++++ 6 files changed, 181 insertions(+), 14 deletions(-) diff --git a/korman/exporter/mesh.py b/korman/exporter/mesh.py index cb7d388..5da7a62 100644 --- a/korman/exporter/mesh.py +++ b/korman/exporter/mesh.py @@ -40,16 +40,12 @@ class _RenderLevel: _MINOR_MASK = ((1 << _MAJOR_SHIFT) - 1) def __init__(self, bo, pass_index, blend_span=False): - self.level = 0 - if pass_index > 0: - self.major = self.MAJOR_FRAMEBUF - self.minor = pass_index * 4 + if blend_span: + self.level = self._determine_level(bo, blend_span) else: - self.major = self.MAJOR_BLEND if blend_span else self.MAJOR_OPAQUE - - # We use the blender material's pass index (which we stashed in the hsGMaterial) to increment - # the render pass, just like it says... - self.level += pass_index + self.level = 0 + # Gulp... Hope you know what you're doing... + self.minor += pass_index * 4 def __eq__(self, other): return self.level == other.level @@ -60,15 +56,38 @@ class _RenderLevel: def _get_major(self): return self.level >> self._MAJOR_SHIFT def _set_major(self, value): - self.level = ((value << self._MAJOR_SHIFT) & 0xFFFFFFFF) | self.minor + self.level = self._calc_level(value, self.minor) major = property(_get_major, _set_major) def _get_minor(self): return self.level & self._MINOR_MASK def _set_minor(self, value): - self.level = ((self.major << self._MAJOR_SHIFT) & 0xFFFFFFFF) | value + self.level = self._calc_level(self.major, value) minor = property(_get_minor, _set_minor) + def _calc_level(self, major : int, minor : int=0) -> int: + return ((major << self._MAJOR_SHIFT) & 0xFFFFFFFF) | minor + + def _determine_level(self, bo : bpy.types.Object, blend_span : bool) -> int: + mods = bo.plasma_modifiers + if mods.test_property("draw_framebuf"): + return self._calc_level(self.MAJOR_FRAMEBUF) + elif mods.test_property("draw_opaque"): + return self._calc_level(self.MAJOR_OPAQUE) + elif mods.test_property("draw_no_defer"): + blend_span = False + + blend_mod = mods.blend + if blend_mod.enabled and blend_mod.has_dependencies: + level = self._calc_level(self.MAJOR_FRAMEBUF) + for i in blend_mod.iter_dependencies(): + level = max(level, self._determine_level(i, blend_span)) + return level + 4 + elif blend_span: + return self._calc_level(self.MAJOR_BLEND) + else: + return self._calc_level(self.MAJOR_DEFAULT) + class _DrawableCriteria: def __init__(self, bo, geospan, pass_index): @@ -96,12 +115,12 @@ class _DrawableCriteria: def _face_sort_allowed(self, bo): # For now, only test the modifiers # This will need to be tweaked further for GUIs... - return not any((i.no_face_sort for i in bo.plasma_modifiers.modifiers)) + return not bo.plasma_modifiers.test_property("no_face_sort") def _span_sort_allowed(self, bo): # For now, only test the modifiers # This will need to be tweaked further for GUIs... - return not any((i.no_face_sort for i in bo.plasma_modifiers.modifiers)) + return not bo.plasma_modifiers.test_property("no_face_sort") @property def span_type(self): @@ -118,7 +137,6 @@ class _GeoData: self.vertices = [] - class _MeshManager: def __init__(self, report=None): if report is not None: diff --git a/korman/idprops.py b/korman/idprops.py index 66328d2..b7c55a6 100644 --- a/korman/idprops.py +++ b/korman/idprops.py @@ -127,6 +127,9 @@ def poll_animated_objects(self, value): def poll_camera_objects(self, value): return value.type == "CAMERA" +def poll_drawable_objects(self, value): + return value.type == "MESH" and any(value.data.materials) + def poll_empty_objects(self, value): return value.type == "EMPTY" diff --git a/korman/properties/modifiers/__init__.py b/korman/properties/modifiers/__init__.py index f4905d3..ba179df 100644 --- a/korman/properties/modifiers/__init__.py +++ b/korman/properties/modifiers/__init__.py @@ -66,6 +66,10 @@ class PlasmaModifiers(bpy.types.PropertyGroup): setattr(cls, i.pl_id, bpy.props.PointerProperty(type=i)) bpy.types.Object.plasma_modifiers = bpy.props.PointerProperty(type=cls) + def test_property(self, property : str) -> bool: + """Tests a property on all enabled Plasma modifiers""" + return any((getattr(i, property) for i in self.modifiers)) + class PlasmaModifierSpec(bpy.types.PropertyGroup): pass diff --git a/korman/properties/modifiers/base.py b/korman/properties/modifiers/base.py index 5f4ea34..0f52486 100644 --- a/korman/properties/modifiers/base.py +++ b/korman/properties/modifiers/base.py @@ -30,10 +30,30 @@ class PlasmaModifierProperties(bpy.types.PropertyGroup): def destroyed(self): pass + @property + def draw_opaque(self): + """Render geometry before the avatar""" + return False + + @property + def draw_framebuf(self): + """Render geometry after the avatar but before other blended geometry""" + return False + + @property + def draw_no_defer(self): + """Disallow geometry being sorted into a blending span""" + return False + @property def enabled(self): return self.display_order >= 0 + @property + def face_sort(self): + """Indicates that the geometry's faces should be sorted by the engine""" + return False + def harvest_actors(self): return () diff --git a/korman/properties/modifiers/render.py b/korman/properties/modifiers/render.py index 856840c..f49db3c 100644 --- a/korman/properties/modifiers/render.py +++ b/korman/properties/modifiers/render.py @@ -25,6 +25,98 @@ from ...exporter import utils from ...exporter.explosions import ExportError from ... import idprops +class PlasmaBlendOntoObject(bpy.types.PropertyGroup): + blend_onto = PointerProperty(name="Blend Onto", + description="Object to render first", + options=set(), + type=bpy.types.Object, + poll=idprops.poll_drawable_objects) + enabled = BoolProperty(name="Enabled", + default=True, + options=set()) + + +class PlasmaBlendMod(PlasmaModifierProperties): + pl_id = "blend" + + bl_category = "Render" + bl_label = "Blending" + bl_description = "Advanced Blending Options" + + render_level = EnumProperty(name="Render Pass", + description="Suggested render pass for this object.", + items=[("AUTO", "(Auto)", "Let Korman decide when to render this object."), + ("OPAQUE", "Before Avatar", "Prefer for the object to draw before the avatar."), + ("FRAMEBUF", "Frame Buffer", "Prefer for the object to draw after the avatar but before other blended objects."), + ("BLEND", "Blended", "Prefer for the object to draw after most other geometry in the blended pass.")], + options=set()) + sort_faces = EnumProperty(name="Sort Faces", + description="", + items=[("AUTO", "(Auto)", "Let Korman decide if faces should be sorted."), + ("ALWAYS", "Always", "Force the object's faces to be sorted."), + ("NEVER", "Never", "Force the object's faces to never be sorted.")], + options=set()) + + dependencies = CollectionProperty(type=PlasmaBlendOntoObject) + active_dependency_index = IntProperty(options={"HIDDEN"}) + + def export(self, exporter, bo, so): + # What'er you lookin at? + pass + + @property + def draw_opaque(self): + return self.render_level == "OPAQUE" + + @property + def draw_framebuf(self): + return self.render_level == "FRAMEBUF" + + @property + def draw_no_defer(self): + return self.render_level != "BLEND" + + @property + def face_sort(self): + return self.sort_faces == "ALWAYS" + + @property + def no_face_sort(self): + return self.sort_faces == "NEVER" + + @property + def has_dependencies(self): + return bool(self.dependencies) + + @property + def has_circular_dependency(self): + return self._check_circular_dependency() + + def _check_circular_dependency(self, objects=None): + if objects is None: + objects = set() + elif self.name in objects: + return True + objects.add(self.name) + + for i in self.iter_dependencies(): + # New deep copy of the set for each dependency, so an object can be reused as a + # dependant's dependant. + this_branch = set(objects) + sub_mod = i.plasma_modifiers.blend + if sub_mod.enabled and sub_mod._check_circular_dependency(this_branch): + return True + return False + + def iter_dependencies(self): + for i in (j.blend_onto for j in self.dependencies if j.blend_onto is not None and j.enabled): + yield i + + def sanity_check(self): + if self.has_circular_dependency: + raise ExportError("'{}': Circular Render Dependency detected!".format(self.name)) + + class PlasmaDecalManagerRef(bpy.types.PropertyGroup): enabled = BoolProperty(name="Enabled", default=True, diff --git a/korman/ui/modifiers/render.py b/korman/ui/modifiers/render.py index 64d3c9b..7c6dfa6 100644 --- a/korman/ui/modifiers/render.py +++ b/korman/ui/modifiers/render.py @@ -18,6 +18,36 @@ import bpy from .. import ui_list from ...exporter.mesh import _VERTEX_COLOR_LAYERS +class BlendOntoListUI(bpy.types.UIList): + def draw_item(self, context, layout, data, item, icon, active_data, active_property, index=0, flt_flag=0): + if item.blend_onto is None: + layout.label("[No Object Specified]", icon="ERROR") + else: + layout.label(item.blend_onto.name, icon="OBJECT_DATA") + layout.prop(item, "enabled", text="") + + +def blend(modifier, layout, context): + # Warn if there are render dependencies and a manual render level specification -- this + # could lead to unpredictable results. + layout.alert = modifier.render_level != "AUTO" and bool(modifier.dependencies) + layout.prop(modifier, "render_level") + layout.alert = False + layout.prop(modifier, "sort_faces") + + layout.separator() + layout.label("Render Dependencies:") + ui_list.draw_modifier_list(layout, "BlendOntoListUI", modifier, "dependencies", + "active_dependency_index", rows=2, maxrows=4) + try: + dependency_ref = modifier.dependencies[modifier.active_dependency_index] + except: + pass + else: + layout.alert = dependency_ref.blend_onto is None + layout.prop(dependency_ref, "blend_onto") + + class DecalMgrListUI(bpy.types.UIList): def draw_item(self, context, layout, data, item, icon, active_data, active_property, index=0, flt_flag=0): if item.name: From 94398fd293b84b70f0dc44ddc2cb64e66bc86073 Mon Sep 17 00:00:00 2001 From: Adam Johnson Date: Wed, 30 Sep 2020 13:16:41 -0400 Subject: [PATCH 17/50] Improve support for VtxNonPreshaded and RT Lights. This appropriately marks spans as VtxNonPreshaded if they use vertex alpha or runtime lighting. In those cases, we properly fold the runtime light color into the vertex color as expected by Plasma. --- korman/exporter/mesh.py | 169 +++++++++++++++++++++++++++++----------- 1 file changed, 125 insertions(+), 44 deletions(-) diff --git a/korman/exporter/mesh.py b/korman/exporter/mesh.py index 5da7a62..491af68 100644 --- a/korman/exporter/mesh.py +++ b/korman/exporter/mesh.py @@ -14,6 +14,7 @@ # along with Korman. If not, see . import bpy +import itertools from PyHSPlasma import * from math import fabs import weakref @@ -29,6 +30,29 @@ _WARN_VERTS_PER_SPAN = 0x8000 _VERTEX_COLOR_LAYERS = {"col", "color", "colour"} +class _GeoSpan: + def __init__(self, bo, bm, geospan, pass_index=None): + self.geospan = geospan + self.pass_index = pass_index if pass_index is not None else 0 + self.mult_color = self._determine_mult_color(bo, bm) + + def _determine_mult_color(self, bo, bm): + """Determines the color all vertex colors should be multipled by in this span.""" + if self.geospan.props & plGeometrySpan.kDiffuseFoldedIn: + color = bm.diffuse_color + base_layer = self._find_bottom_of_stack() + return (color.r, color.b, color.g, base_layer.opacity) + if not bo.plasma_modifiers.lighting.preshade: + return (0.0, 0.0, 0.0, 0.0) + return (1.0, 1.0, 1.0, 1.0) + + def _find_bottom_of_stack(self) -> plLayerInterface: + base_layer = self.geospan.material.object.layers[0].object + while base_layer.underLay is not None: + base_layer = base_layer.underLay.object + return base_layer + + class _RenderLevel: MAJOR_OPAQUE = 0 MAJOR_FRAMEBUF = 1 @@ -231,15 +255,48 @@ class MeshConverter(_MeshManager): return (num_user_texs, total_texs, max_user_texs) - def _create_geospan(self, bo, mesh, bm, hsgmatKey): + def _check_vtx_alpha(self, mesh, material_idx): + if material_idx is not None: + polygons = (i for i in mesh.polygons if i.material_index == material_idx) + else: + polygons = mesh.polygons + alpha_layer = self._find_vtx_alpha_layer(mesh.vertex_colors) + if alpha_layer is None: + return False + alpha_loops = (alpha_layer[i.loop_start:i.loop_start+i.loop_total] for i in polygons) + opaque = (sum(i.color) == len(i.color) for i in itertools.chain.from_iterable(alpha_loops)) + has_alpha = not all(opaque) + return has_alpha + + def _check_vtx_nonpreshaded(self, bo, mesh, material_idx, base_layer): + def check_layer_shading_animation(layer): + if isinstance(layer, plLayerAnimationBase): + return layer.opacityCtl is not None or layer.preshadeCtl is not None or layer.runtimeCtl is not None + if layer.underLay is not None: + return check_layer_shading_animation(layer.underLay.object) + return False + + # TODO: if this is an avatar, we can't be non-preshaded. + if check_layer_shading_animation(base_layer): + return False + if base_layer.state.shadeFlags & hsGMatState.kShadeEmissive: + return False + + mods = bo.plasma_modifiers + if mods.lighting.rt_lights: + return True + if mods.lightmap.bake_lightmap: + return True + if self._check_vtx_alpha(mesh, material_idx): + return True + + return False + + def _create_geospan(self, bo, mesh, material_idx, bm, hsgmatKey): """Initializes a plGeometrySpan from a Blender Object and an hsGMaterial""" geospan = plGeometrySpan() geospan.material = hsgmatKey - # Mark us as needing a BlendSpan if the material require blending - if hsgmatKey.object.layers[0].object.state.blendFlags & hsGMatState.kBlendMask: - geospan.props |= plGeometrySpan.kRequiresBlending - # GeometrySpan format # For now, we really only care about the number of UVW Channels user_uvws, total_uvws, max_user_uvws = self._calc_num_uvchans(bo, mesh) @@ -247,10 +304,22 @@ class MeshConverter(_MeshManager): raise explosions.TooManyUVChannelsError(bo, bm, user_uvws, max_user_uvws) geospan.format = total_uvws - # Begin total guesswork WRT flags - mods = bo.plasma_modifiers - if mods.lightmap.enabled: + def is_alpha_blended(layer): + if layer.state.blendFlags & hsGMatState.kBlendMask: + return True + if layer.underLay is not None: + return is_alpha_blended(layer.underLay.object) + return False + + base_layer = hsgmatKey.object.layers[0].object + if is_alpha_blended(base_layer) or self._check_vtx_alpha(mesh, material_idx): + geospan.props |= plGeometrySpan.kRequiresBlending + if self._check_vtx_nonpreshaded(bo, mesh, material_idx, base_layer): geospan.props |= plGeometrySpan.kLiteVtxNonPreshaded + if (geospan.props & plGeometrySpan.kLiteMask) != plGeometrySpan.kLiteMaterial: + geospan.props |= plGeometrySpan.kDiffuseFoldedIn + + mods = bo.plasma_modifiers if mods.lighting.rt_lights: geospan.props |= plGeometrySpan.kPropRunTimeLight if not bm.use_shadows: @@ -292,7 +361,7 @@ class MeshConverter(_MeshManager): dspan.composeGeometry(True, True) inc_progress() - def _export_geometry(self, bo, mesh, materials, geospans): + def _export_geometry(self, bo, mesh, materials, geospans, mat2span_LUT): # Recall that materials is a mapping of exported materials to blender material indices. # Therefore, geodata maps blender material indices to working geometry data. # Maybe the logic is a bit inverted, but it keeps the inner loop simple. @@ -301,16 +370,8 @@ class MeshConverter(_MeshManager): # Locate relevant vertex color layers now... lm = bo.plasma_modifiers.lightmap - has_vtx_alpha = False - color, alpha = None, None - for vcol_layer in mesh.tessface_vertex_colors: - name = vcol_layer.name.lower() - if name in _VERTEX_COLOR_LAYERS: - color = vcol_layer.data - elif name == "autocolor" and color is None and not lm.bake_lightmap: - color = vcol_layer.data - elif name == "alpha": - alpha = vcol_layer.data + color = None if lm.bake_lightmap else self._find_vtx_color_layer(mesh.tessface_vertex_colors) + alpha = self._find_vtx_alpha_layer(mesh.tessface_vertex_colors) # Convert Blender faces into things we can stuff into libHSPlasma for i, tessface in enumerate(mesh.tessfaces): @@ -340,10 +401,8 @@ class MeshConverter(_MeshManager): else: src = alpha[i] # average color becomes the alpha value - tessface_alphas = (((src.color1[0] + src.color1[1] + src.color1[2]) / 3), - ((src.color2[0] + src.color2[1] + src.color2[2]) / 3), - ((src.color3[0] + src.color3[1] + src.color3[2]) / 3), - ((src.color4[0] + src.color4[1] + src.color4[2]) / 3)) + tessface_alphas = ((sum(src.color1) / 3), (sum(src.color2) / 3), + (sum(src.color3) / 3), (sum(src.color4) / 3)) if bumpmap is not None: gradPass = [] @@ -373,10 +432,16 @@ class MeshConverter(_MeshManager): for j, vertex in enumerate(tessface.vertices): uvws = tuple([uvw[j] for uvw in tessface_uvws]) - # Grab VCols - vertex_color = (int(tessface_colors[j][0] * 255), int(tessface_colors[j][1] * 255), - int(tessface_colors[j][2] * 255), int(tessface_alphas[j] * 255)) - has_vtx_alpha |= bool(tessface_alphas[j] < 1.0) + # Calculate vertex colors. + if mat2span_LUT: + mult_color = geospans[mat2span_LUT[tessface.material_index]].mult_color + else: + mult_color = (1.0, 1.0, 1.0, 1.0) + tessface_color, tessface_alpha = tessface_colors[j], tessface_alphas[j] + vertex_color = (int(tessface_color[0] * mult_color[0] * 255), + int(tessface_color[1] * mult_color[1] * 255), + int(tessface_color[2] * mult_color[2] * 255), + int(tessface_alpha * mult_color[0] * 255)) # Now, we'll index into the vertex dict using the per-face elements :( # We're using tuples because lists are not hashable. The many mathutils and PyHSPlasma @@ -433,7 +498,7 @@ class MeshConverter(_MeshManager): # Time to finish it up... for i, data in enumerate(geodata.values()): - geospan = geospans[i][0] + geospan = geospans[i].geospan numVerts = len(data.vertices) numUVs = geospan.format & plGeometrySpan.kUVCountMask @@ -454,10 +519,6 @@ class MeshConverter(_MeshManager): uvMap[numUVs - 1].normalize() vtx.uvs = uvMap - # Mark us for blending if we have a alpha vertex paint layer - if has_vtx_alpha: - geospan.props |= plGeometrySpan.kRequiresBlending - # If we're still here, let's add our data to the GeometrySpan geospan.indices = data.triangles geospan.vertices = data.vertices @@ -540,18 +601,18 @@ class MeshConverter(_MeshManager): return None # Step 1: Export all of the doggone materials. - geospans = self._export_material_spans(bo, mesh, materials) + geospans, mat2span_LUT = self._export_material_spans(bo, mesh, materials) # Step 2: Export Blender mesh data to Plasma GeometrySpans - self._export_geometry(bo, mesh, materials, geospans) + self._export_geometry(bo, mesh, materials, geospans, mat2span_LUT) # Step 3: Add plGeometrySpans to the appropriate DSpan and create indices _diindices = {} - for geospan, pass_index in geospans: - dspan = self._find_create_dspan(bo, geospan, pass_index) + for i in geospans: + dspan = self._find_create_dspan(bo, i.geospan, i.pass_index) self._report.msg("Exported hsGMaterial '{}' geometry into '{}'", - geospan.material.name, dspan.key.name, indent=1) - idx = dspan.addSourceSpan(geospan) + i.geospan.material.name, dspan.key.name, indent=1) + idx = dspan.addSourceSpan(i.geospan) diidx = _diindices.setdefault(dspan, []) diidx.append(idx) @@ -571,20 +632,25 @@ class MeshConverter(_MeshManager): if len(materials) > 1: msg = "'{}' is a WaveSet -- only one material is supported".format(bo.name) self._exporter().report.warn(msg, indent=1) - matKey = self.material.export_waveset_material(bo, materials[0][1]) - geospan = self._create_geospan(bo, mesh, materials[0][1], matKey) + blmat = materials[0][1] + matKey = self.material.export_waveset_material(bo, blmat) + geospan = self._create_geospan(bo, mesh, None, blmat, matKey) # FIXME: Can some of this be generalized? geospan.props |= (plGeometrySpan.kWaterHeight | plGeometrySpan.kLiteVtxNonPreshaded | plGeometrySpan.kPropReverseSort | plGeometrySpan.kPropNoShadow) geospan.waterHeight = bo.location[2] - return [(geospan, 0)] + return [_GeoSpan(bo, blmat, geospan)], None else: geospans = [None] * len(materials) - for i, (_, blmat) in enumerate(materials): + mat2span_LUT = {} + for i, (blmat_idx, blmat) in enumerate(materials): matKey = self.material.export_material(bo, blmat) - geospans[i] = (self._create_geospan(bo, mesh, blmat, matKey), blmat.pass_index) - return geospans + geospans[i] = _GeoSpan(bo, blmat, + self._create_geospan(bo, mesh, blmat_idx, blmat, matKey), + blmat.pass_index) + mat2span_LUT[blmat_idx] = i + return geospans, mat2span_LUT def _find_create_dspan(self, bo, geospan, pass_index): location = self._mgr.get_location(bo) @@ -620,6 +686,21 @@ class MeshConverter(_MeshManager): else: return self._dspans[location][crit] + def _find_vtx_alpha_layer(self, color_collection): + alpha_layer = next((i for i in color_collection if i.name.lower() == "alpha"), None) + if alpha_layer is not None: + return alpha_layer.data + return None + + def _find_vtx_color_layer(self, color_collection): + manual_layer = next((i for i in color_collection if i.name.lower() in _VERTEX_COLOR_LAYERS), None) + if manual_layer is not None: + return manual_layer.data + baked_layer = color_collection.get("autocolor") + if baked_layer is not None: + return baked_layer.data + return None + @property def _mgr(self): return self._exporter().mgr From a203e109ba5c5d3f72564cbfe0acf4ea3b442570 Mon Sep 17 00:00:00 2001 From: Adam Johnson Date: Wed, 30 Sep 2020 20:39:09 -0400 Subject: [PATCH 18/50] Properly set the layer colors and emissive flag. This fixes the layer preshade color to be black when runtime lighting is requested. Further, both preshade and runtime are properly set to black for emissive layers. --- korman/exporter/material.py | 55 +++++++++++++++++++++++++------------ korman/exporter/mesh.py | 2 +- 2 files changed, 39 insertions(+), 18 deletions(-) diff --git a/korman/exporter/material.py b/korman/exporter/material.py index 4b973e4..9bdc221 100644 --- a/korman/exporter/material.py +++ b/korman/exporter/material.py @@ -147,6 +147,16 @@ class MaterialConverter: "transformCtl": self._export_layer_transform_animation, } + def calc_material_ambient(self, bo, bm) -> hsColorRGBA: + emit_scale = bm.emit * 0.5 + if emit_scale > 0.0: + return hsColorRGBA(bm.diffuse_color.r * emit_scale, + bm.diffuse_color.g * emit_scale, + bm.diffuse_color.b * emit_scale, + 1.0) + else: + return utils.color(bpy.context.scene.world.ambient_color) + def _can_export_texslot(self, slot): if slot is None or not slot.use: return False @@ -187,7 +197,9 @@ class MaterialConverter: self._report.msg("Exporting Material '{}' as single user '{}'", bm.name, mat_name, indent=1) hgmat = None else: - mat_name = bm.name + # Ensure that RT-lit objects don't infect the static-lit objects. + mat_prefix = "RTLit_" if bo.plasma_modifiers.lighting.rt_lights else "" + mat_name = "".join((mat_prefix, bm.name)) self._report.msg("Exporting Material '{}'", mat_name, indent=1) hsgmat = self._mgr.find_key(hsGMaterial, name=mat_name, bl=bo) if hsgmat is not None: @@ -222,7 +234,8 @@ class MaterialConverter: if slot.use_stencil: stencils.append((idx, slot)) else: - tex_layer = self.export_texture_slot(bo, bm, hsgmat, slot, idx) + tex_name = "{}_{}".format(mat_name, slot.name) + tex_layer = self.export_texture_slot(bo, bm, hsgmat, slot, idx, name=tex_name) if restart_pass_next: tex_layer.state.miscFlags |= hsGMatState.kMiscRestartPassHere restart_pass_next = False @@ -249,7 +262,7 @@ class MaterialConverter: # Plasma makes several assumptions that every hsGMaterial has at least one layer. If this # material had no Textures, we will need to initialize a default layer if not hsgmat.layers: - layer = self._mgr.find_create_object(plLayer, name="{}_AutoLayer".format(bm.name), bl=bo) + layer = self._mgr.find_create_object(plLayer, name="{}_AutoLayer".format(mat_name), bl=bo) self._propagate_material_settings(bo, bm, layer) hsgmat.addLayer(layer.key) @@ -349,7 +362,7 @@ class MaterialConverter: return hsgmat.key def export_bumpmap_slot(self, bo, bm, hsgmat, slot, idx): - name = "{}_{}".format(bm.name if bm is not None else bo.name, slot.name) + name = "{}_{}".format(hsgmat.key.name, slot.name) self._report.msg("Exporting Plasma Bumpmap Layers for '{}'", name, indent=2) # Okay, now we need to make 3 layers for the Du, Dw, and Dv @@ -1163,6 +1176,20 @@ class MaterialConverter: def get_bump_layer(self, bo): return self._bump_mats.get(bo, None) + def get_material_preshade(self, bo, bm, color=None) -> hsColorRGBA: + if bo.plasma_modifiers.lighting.rt_lights or bm.emit: + return hsColorRGBA.kBlack + if color is None: + color = bm.diffuse_color + return utils.color(color) + + def get_material_runtime(self, bo, bm, color=None) -> hsColorRGBA: + if bm.emit: + return hsColorRGBA.kBlack + if color is None: + color = bm.diffuse_color + return utils.color(color) + def get_texture_animation_key(self, bo, bm, texture): """Finds or creates the appropriate key for sending messages to an animated Texture""" @@ -1204,23 +1231,17 @@ class MaterialConverter: if bm.use_shadeless: state.shadeFlags |= hsGMatState.kShadeWhite + if bm.emit: + state.shadeFlags |= hsGMatState.kShadeEmissive + # Colors - layer.ambient = utils.color(bpy.context.scene.world.ambient_color) - layer.preshade = utils.color(bm.diffuse_color) - layer.runtime = utils.color(bm.diffuse_color) + layer.ambient = self.calc_material_ambient(bo, bm) + layer.preshade = self.get_material_preshade(bo, bm) + layer.runtime = self.get_material_runtime(bo, bm) layer.specular = utils.color(bm.specular_color) layer.specularPower = min(100.0, float(bm.specular_hardness)) - layer.LODBias = -1.0 # Seems to be the Plasma default - - if bm.emit > 0.0: - # Use the diffuse colour as the emit, scaled by the emit amount - # (maximum 2.0, so we'll also scale that by 0.5) - emit_scale = bm.emit * 0.5 - layer.ambient = hsColorRGBA(bm.diffuse_color.r * emit_scale, - bm.diffuse_color.g * emit_scale, - bm.diffuse_color.b * emit_scale, - 1.0) + layer.LODBias = -1.0 def _requires_single_user(self, bo, bm): if bo.data.show_double_sided: diff --git a/korman/exporter/mesh.py b/korman/exporter/mesh.py index 491af68..1f1dc7d 100644 --- a/korman/exporter/mesh.py +++ b/korman/exporter/mesh.py @@ -279,7 +279,7 @@ class MeshConverter(_MeshManager): # TODO: if this is an avatar, we can't be non-preshaded. if check_layer_shading_animation(base_layer): return False - if base_layer.state.shadeFlags & hsGMatState.kShadeEmissive: + if material_idx is not None and mesh.materials[material_idx].emit: return False mods = bo.plasma_modifiers From b0c552ae6a76f556403e2c265529afb873442c52 Mon Sep 17 00:00:00 2001 From: Adam Johnson Date: Wed, 30 Sep 2020 20:40:24 -0400 Subject: [PATCH 19/50] Kickables should be runtime lit. --- korman/properties/modifiers/render.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/korman/properties/modifiers/render.py b/korman/properties/modifiers/render.py index f49db3c..3683039 100644 --- a/korman/properties/modifiers/render.py +++ b/korman/properties/modifiers/render.py @@ -531,6 +531,8 @@ class PlasmaLightingMod(PlasmaModifierProperties): return True if self.id_data.plasma_object.has_transform_animation: return True + if mods.collision.enabled and mods.collision.dynamic: + return True return False From 8f3a94589e0b204412069664f94f9d545f2f79d0 Mon Sep 17 00:00:00 2001 From: Mark Eggert <72320499+TikiBear@users.noreply.github.com> Date: Sun, 4 Oct 2020 12:19:45 -0700 Subject: [PATCH 20/50] Update mesh.py Prevent export of Blender Decimate modifier from failing and corrupting Blender file! --- korman/exporter/mesh.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/korman/exporter/mesh.py b/korman/exporter/mesh.py index cb7d388..e5392f0 100644 --- a/korman/exporter/mesh.py +++ b/korman/exporter/mesh.py @@ -178,7 +178,7 @@ class _MeshManager: for cached_mod in override["modifiers"]: mod = bo.modifiers.new(cached_mod["name"], cached_mod["type"]) for key, value in cached_mod.items(): - if key in {"name", "type"}: + if key in {"name", "type"} or (cached_mod["type"] == "DECIMATE" and key=="face_count"): # Decimate attribute face_count is read-only continue setattr(mod, key, value) From 9b061154dc687e82fb00e91f050c62480a0f7a52 Mon Sep 17 00:00:00 2001 From: Mark Eggert <72320499+TikiBear@users.noreply.github.com> Date: Mon, 5 Oct 2020 12:45:44 -0700 Subject: [PATCH 21/50] Update mesh.py Fix Blender Decimate mod crash. Better syntax. --- korman/exporter/mesh.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/korman/exporter/mesh.py b/korman/exporter/mesh.py index e5392f0..b0a5da2 100644 --- a/korman/exporter/mesh.py +++ b/korman/exporter/mesh.py @@ -174,11 +174,12 @@ class _MeshManager: trash_mesh, bo.data = bo.data, data_meshes.get(override["mesh"]) data_meshes.remove(trash_mesh) - # If modifiers were removed, reapply them now. + # If modifiers were removed, reapply them now unless they're read-only. + readonly_attributes = {("DECIMATE", "face_count"),} for cached_mod in override["modifiers"]: mod = bo.modifiers.new(cached_mod["name"], cached_mod["type"]) for key, value in cached_mod.items(): - if key in {"name", "type"} or (cached_mod["type"] == "DECIMATE" and key=="face_count"): # Decimate attribute face_count is read-only + if key in {"name", "type"} or (cached_mod["type"], key) in readonly_attributes: continue setattr(mod, key, value) From 3abb36b4c4629266ef1a935f44bdce406c83d828 Mon Sep 17 00:00:00 2001 From: Adam Johnson Date: Mon, 5 Oct 2020 19:13:06 -0400 Subject: [PATCH 22/50] Partially revert the emissive stuff. --- korman/exporter/material.py | 26 +++++++++++++------------- korman/exporter/mesh.py | 9 +++++++-- 2 files changed, 20 insertions(+), 15 deletions(-) diff --git a/korman/exporter/material.py b/korman/exporter/material.py index 9bdc221..d2a2ba9 100644 --- a/korman/exporter/material.py +++ b/korman/exporter/material.py @@ -147,16 +147,6 @@ class MaterialConverter: "transformCtl": self._export_layer_transform_animation, } - def calc_material_ambient(self, bo, bm) -> hsColorRGBA: - emit_scale = bm.emit * 0.5 - if emit_scale > 0.0: - return hsColorRGBA(bm.diffuse_color.r * emit_scale, - bm.diffuse_color.g * emit_scale, - bm.diffuse_color.b * emit_scale, - 1.0) - else: - return utils.color(bpy.context.scene.world.ambient_color) - def _can_export_texslot(self, slot): if slot is None or not slot.use: return False @@ -1176,15 +1166,25 @@ class MaterialConverter: def get_bump_layer(self, bo): return self._bump_mats.get(bo, None) + def get_material_ambient(self, bo, bm) -> hsColorRGBA: + emit_scale = bm.emit * 0.5 + if emit_scale > 0.0: + return hsColorRGBA(bm.diffuse_color.r * emit_scale, + bm.diffuse_color.g * emit_scale, + bm.diffuse_color.b * emit_scale, + 1.0) + else: + return utils.color(bpy.context.scene.world.ambient_color) + def get_material_preshade(self, bo, bm, color=None) -> hsColorRGBA: - if bo.plasma_modifiers.lighting.rt_lights or bm.emit: + 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=None) -> hsColorRGBA: - if bm.emit: + if not bo.plasma_modifiers.lighting.rt_lights: return hsColorRGBA.kBlack if color is None: color = bm.diffuse_color @@ -1235,7 +1235,7 @@ class MaterialConverter: state.shadeFlags |= hsGMatState.kShadeEmissive # Colors - layer.ambient = self.calc_material_ambient(bo, bm) + layer.ambient = self.get_material_ambient(bo, bm) layer.preshade = self.get_material_preshade(bo, bm) layer.runtime = self.get_material_runtime(bo, bm) layer.specular = utils.color(bm.specular_color) diff --git a/korman/exporter/mesh.py b/korman/exporter/mesh.py index 1f1dc7d..03625ba 100644 --- a/korman/exporter/mesh.py +++ b/korman/exporter/mesh.py @@ -279,8 +279,13 @@ class MeshConverter(_MeshManager): # TODO: if this is an avatar, we can't be non-preshaded. if check_layer_shading_animation(base_layer): return False - if material_idx is not None and mesh.materials[material_idx].emit: - 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: From 4f18f64fe795836c1057c3797a6e09803bcd4b33 Mon Sep 17 00:00:00 2001 From: Adam Johnson Date: Thu, 8 Oct 2020 19:49:44 -0400 Subject: [PATCH 23/50] Adjust linking book mod MSB to match Cyan's. This fixes no readily apparent bug, but it's best to match the existing data when we can to prevent future GOTCHAs. --- korman/properties/modifiers/gui.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/korman/properties/modifiers/gui.py b/korman/properties/modifiers/gui.py index 069aa16..a9a94da 100644 --- a/korman/properties/modifiers/gui.py +++ b/korman/properties/modifiers/gui.py @@ -578,6 +578,8 @@ class PlasmaLinkingBookModifier(PlasmaModifierProperties, PlasmaModifierLogicWiz anim_stage = nodes.new("PlasmaAnimStageNode") anim_stage.anim_name = "LinkOut" anim_settings = nodes.new("PlasmaAnimStageSettingsNode") + anim_settings.forward = "kPlayAuto" + anim_settings.stage_advance = "kAdvanceAuto" anim_stage.link_input(anim_settings, "stage", "stage_settings") msb = nodes.new("PlasmaMultiStageBehaviorNode") @@ -616,6 +618,8 @@ class PlasmaLinkingBookModifier(PlasmaModifierProperties, PlasmaModifierLogicWiz anim_stage = nodes.new("PlasmaAnimStageNode") anim_stage.anim_name = "LinkOut" anim_settings = nodes.new("PlasmaAnimStageSettingsNode") + anim_settings.forward = "kPlayAuto" + anim_settings.stage_advance = "kAdvanceAuto" anim_stage.link_input(anim_settings, "stage", "stage_settings") msb = nodes.new("PlasmaMultiStageBehaviorNode") From 32e2553d36c83dfb6dce98f8368cbf09ae73785b Mon Sep 17 00:00:00 2001 From: Patrick Dulebohn Date: Fri, 9 Oct 2020 07:43:21 -0400 Subject: [PATCH 24/50] Add plAgeGlobalAnim support Adds SDL Global animation to Korman --- korman/exporter/animation.py | 35 ++++++++++++++++++++--------- korman/properties/modifiers/anim.py | 3 +++ korman/ui/modifiers/anim.py | 3 +++ 3 files changed, 30 insertions(+), 11 deletions(-) diff --git a/korman/exporter/animation.py b/korman/exporter/animation.py index 6740da9..d71fab9 100644 --- a/korman/exporter/animation.py +++ b/korman/exporter/animation.py @@ -72,7 +72,11 @@ class AnimationConverter: # There is a race condition in the client with animation loading. It expects for modifiers # to be listed on the SceneObject in a specific order. D'OH! So, always use these funcs. agmod, agmaster = self.get_anigraph_objects(bo, so) - atcanim = self._mgr.find_create_object(plATCAnim, so=so) + anim_data = bo.plasma_modifiers.animation + if anim_data.obj_sdl_anim: + atcanim = self._mgr.find_create_object(plAgeGlobalAnim, so=so) + else: + atcanim = self._mgr.find_create_object(plATCAnim, so=so) # Add the animation data to the ATC for i in applicators: @@ -89,21 +93,30 @@ class AnimationConverter: if i is not None: yield i.frame_range[index] atcanim.name = "(Entire Animation)" - 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))) + if anim_data.obj_sdl_anim: + atcanim.start = self._convert_frame_time(min(get_ranges(obj_action, index=0))) + atcanim.end = self._convert_frame_time(max(get_ranges(obj_action, index=1))) + else: + atcanim.start = self._convert_frame_time(min(get_ranges(obj_action, data_action, index=0))) + atcanim.end = self._convert_frame_time(max(get_ranges(obj_action, data_action, index=1))) # Marker points - if obj_action is not None: + if obj_action is not None and anim_data.obj_sdl_anim is None: for marker in obj_action.pose_markers: atcanim.setMarker(marker.name, self._convert_frame_time(marker.frame)) - # Fixme? Not sure if we really need to expose this... - atcanim.easeInMin = 1.0 - atcanim.easeInMax = 1.0 - atcanim.easeInLength = 1.0 - atcanim.easeOutMin = 1.0 - atcanim.easeOutMax = 1.0 - atcanim.easeOutLength = 1.0 + def set_anim_params(plAGAnim): + sdl_name = bo.plasma_modifiers.animation.obj_sdl_anim + if sdl_name: + atcanim.globalVarName = sdl_name + else: + # Fixme? Not sure if we really need to expose this... + atcanim.easeInMin = 1.0 + atcanim.easeInMax = 1.0 + atcanim.easeInLength = 1.0 + atcanim.easeOutMin = 1.0 + atcanim.easeOutMax = 1.0 + atcanim.easeOutLength = 1.0 def _convert_camera_animation(self, bo, so, obj_fcurves, data_fcurves): if data_fcurves: diff --git a/korman/properties/modifiers/anim.py b/korman/properties/modifiers/anim.py index 78047b5..5ce8a41 100644 --- a/korman/properties/modifiers/anim.py +++ b/korman/properties/modifiers/anim.py @@ -60,6 +60,9 @@ class PlasmaAnimationModifier(ActionModifier, PlasmaModifierProperties): description="Marker indicating where the default loop begins") loop_end = StringProperty(name="Loop End", description="Marker indicating where the default loop ends") + obj_sdl_anim = StringProperty(name="SDL Animation", + description="Name of the SDL variable to use for this animation", + options=set()) def export(self, exporter, bo, so): action = self.blender_action diff --git a/korman/ui/modifiers/anim.py b/korman/ui/modifiers/anim.py index 5216dda..5decd6d 100644 --- a/korman/ui/modifiers/anim.py +++ b/korman/ui/modifiers/anim.py @@ -43,6 +43,9 @@ def animation(modifier, layout, context): col.enabled = modifier.loop col.prop_search(modifier, "loop_start", action, "pose_markers", icon="PMARKER") col.prop_search(modifier, "loop_end", action, "pose_markers", icon="PMARKER") + + col.label("SDL Animation:") + col.prop(modifier, "obj_sdl_anim", text="") def animation_filter(modifier, layout, context): split = layout.split() From 6bb0b32a3ceb5e47663df023afcad926ac49a52e Mon Sep 17 00:00:00 2001 From: Patrick Dulebohn Date: Fri, 9 Oct 2020 11:23:28 -0400 Subject: [PATCH 25/50] Bring more sanity to animation exporter Rearrange a few things and some sanity checks. --- korman/exporter/animation.py | 29 ++++++++++++++--------------- 1 file changed, 14 insertions(+), 15 deletions(-) diff --git a/korman/exporter/animation.py b/korman/exporter/animation.py index d71fab9..68ac5d5 100644 --- a/korman/exporter/animation.py +++ b/korman/exporter/animation.py @@ -22,7 +22,7 @@ import weakref from . import utils -class AnimationConverter: +class AnimationConverter(plAGAnim): def __init__(self, exporter): self._exporter = weakref.ref(exporter) self._bl_fps = bpy.context.scene.render.fps @@ -75,7 +75,7 @@ class AnimationConverter: anim_data = bo.plasma_modifiers.animation if anim_data.obj_sdl_anim: atcanim = self._mgr.find_create_object(plAgeGlobalAnim, so=so) - else: + elif anim_data.obj_sdl_anim is None: atcanim = self._mgr.find_create_object(plATCAnim, so=so) # Add the animation data to the ATC @@ -96,7 +96,7 @@ class AnimationConverter: if anim_data.obj_sdl_anim: atcanim.start = self._convert_frame_time(min(get_ranges(obj_action, index=0))) atcanim.end = self._convert_frame_time(max(get_ranges(obj_action, index=1))) - else: + elif anim_data.obj_sdl_anim is None: 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))) @@ -105,18 +105,17 @@ class AnimationConverter: for marker in obj_action.pose_markers: atcanim.setMarker(marker.name, self._convert_frame_time(marker.frame)) - def set_anim_params(plAGAnim): - sdl_name = bo.plasma_modifiers.animation.obj_sdl_anim - if sdl_name: - atcanim.globalVarName = sdl_name - else: - # Fixme? Not sure if we really need to expose this... - atcanim.easeInMin = 1.0 - atcanim.easeInMax = 1.0 - atcanim.easeInLength = 1.0 - atcanim.easeOutMin = 1.0 - atcanim.easeOutMax = 1.0 - atcanim.easeOutLength = 1.0 + sdl_name = bo.plasma_modifiers.animation.obj_sdl_anim + if sdl_name: + atcanim.globalVarName = sdl_name + elif sdl_name is None: + # Fixme? Not sure if we really need to expose this... + atcanim.easeInMin = 1.0 + atcanim.easeInMax = 1.0 + atcanim.easeInLength = 1.0 + atcanim.easeOutMin = 1.0 + atcanim.easeOutMax = 1.0 + atcanim.easeOutLength = 1.0 def _convert_camera_animation(self, bo, so, obj_fcurves, data_fcurves): if data_fcurves: From c5552f890ccc6f9ad09a66fdf0e7f6c24efb7d64 Mon Sep 17 00:00:00 2001 From: Patrick Dulebohn Date: Fri, 9 Oct 2020 12:30:30 -0400 Subject: [PATCH 26/50] Separate ATC and AgeGlobal in Props Separates ATC and AgeGlobal animtaions so we don't get any doubles between the two. Also, AgeGlobal doesn't need auto start and loop values as it does that automatically. --- korman/properties/modifiers/anim.py | 52 ++++++++++++++++------------- 1 file changed, 28 insertions(+), 24 deletions(-) diff --git a/korman/properties/modifiers/anim.py b/korman/properties/modifiers/anim.py index 5ce8a41..f59b853 100644 --- a/korman/properties/modifiers/anim.py +++ b/korman/properties/modifiers/anim.py @@ -67,33 +67,37 @@ class PlasmaAnimationModifier(ActionModifier, PlasmaModifierProperties): def export(self, exporter, bo, so): action = self.blender_action - atcanim = exporter.mgr.find_create_object(plATCAnim, so=so) - atcanim.autoStart = self.auto_start - atcanim.loop = self.loop - - # Simple start and loop info - if action is not None: - markers = action.pose_markers - initial_marker = markers.get(self.initial_marker) - if initial_marker is not None: - atcanim.initial = _convert_frame_time(initial_marker.frame) - else: - atcanim.initial = -1.0 - if self.loop: - loop_start = markers.get(self.loop_start) - if loop_start is not None: - atcanim.loopStart = _convert_frame_time(loop_start.frame) + anim_data = bo.plasma_modifiers.animation + if anim_data.obj_sdl_anim: + atcanim = exporter.mgr.find_create_object(plAgeGlobalAnim, so=so) + else: + atcanim = exporter.mgr.find_create_object(plATCAnim, so=so) + atcanim.autoStart = self.auto_start + atcanim.loop = self.loop + + # Simple start and loop info for ATC + if action is not None: + markers = action.pose_markers + initial_marker = markers.get(self.initial_marker) + if initial_marker is not None: + atcanim.initial = _convert_frame_time(initial_marker.frame) else: + atcanim.initial = -1.0 + if self.loop: + loop_start = markers.get(self.loop_start) + if loop_start is not None: + atcanim.loopStart = _convert_frame_time(loop_start.frame) + else: + atcanim.loopStart = atcanim.start + loop_end = markers.get(self.loop_end) + if loop_end is not None: + atcanim.loopEnd = _convert_frame_time(loop_end.frame) + else: + atcanim.loopEnd = atcanim.end + else: + if self.loop: atcanim.loopStart = atcanim.start - loop_end = markers.get(self.loop_end) - if loop_end is not None: - atcanim.loopEnd = _convert_frame_time(loop_end.frame) - else: atcanim.loopEnd = atcanim.end - else: - if self.loop: - atcanim.loopStart = atcanim.start - atcanim.loopEnd = atcanim.end class AnimGroupObject(idprops.IDPropObjectMixin, bpy.types.PropertyGroup): From 1e9ceccc9c85203e6375c3c60e061329951c1a80 Mon Sep 17 00:00:00 2001 From: Patrick Dulebohn Date: Fri, 9 Oct 2020 16:41:33 -0400 Subject: [PATCH 27/50] More fixes per Hoikas Adjusting things according to Hoikas' notes. The globalVarName is once again not exporting with the AgeGlobalAnim. --- korman/exporter/animation.py | 43 ++++++++++++++--------------- korman/properties/modifiers/anim.py | 10 +++++-- 2 files changed, 27 insertions(+), 26 deletions(-) diff --git a/korman/exporter/animation.py b/korman/exporter/animation.py index 68ac5d5..4d06a45 100644 --- a/korman/exporter/animation.py +++ b/korman/exporter/animation.py @@ -22,7 +22,7 @@ import weakref from . import utils -class AnimationConverter(plAGAnim): +class AnimationConverter: def __init__(self, exporter): self._exporter = weakref.ref(exporter) self._bl_fps = bpy.context.scene.render.fps @@ -72,10 +72,10 @@ class AnimationConverter(plAGAnim): # There is a race condition in the client with animation loading. It expects for modifiers # to be listed on the SceneObject in a specific order. D'OH! So, always use these funcs. agmod, agmaster = self.get_anigraph_objects(bo, so) - anim_data = bo.plasma_modifiers.animation - if anim_data.obj_sdl_anim: + anim_mod = bo.plasma_modifiers.animation + if anim_mod.obj_sdl_anim: atcanim = self._mgr.find_create_object(plAgeGlobalAnim, so=so) - elif anim_data.obj_sdl_anim is None: + else: atcanim = self._mgr.find_create_object(plATCAnim, so=so) # Add the animation data to the ATC @@ -93,30 +93,27 @@ class AnimationConverter(plAGAnim): if i is not None: yield i.frame_range[index] atcanim.name = "(Entire Animation)" - if anim_data.obj_sdl_anim: - atcanim.start = self._convert_frame_time(min(get_ranges(obj_action, index=0))) - atcanim.end = self._convert_frame_time(max(get_ranges(obj_action, index=1))) - elif anim_data.obj_sdl_anim is None: - 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))) + def anim_range_type(plAGAnim): + if anim_mod.obj_sdl_anim: + atcanim.start = self._convert_frame_time(min(get_ranges(obj_action, data_action, index=0))) + atcanim.end = self._convert_frame_time(max(get_ranges(obj_action, data_action, index=1))) + atcanim.globalVarName = anim_mod.obj_sdl_anim + else: + 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))) + # Fixme? Not sure if we really need to expose this... + atcanim.easeInMin = 1.0 + atcanim.easeInMax = 1.0 + atcanim.easeInLength = 1.0 + atcanim.easeOutMin = 1.0 + atcanim.easeOutMax = 1.0 + atcanim.easeOutLength = 1.0 # Marker points - if obj_action is not None and anim_data.obj_sdl_anim is None: + if obj_action is not None and anim_mod is None: for marker in obj_action.pose_markers: atcanim.setMarker(marker.name, self._convert_frame_time(marker.frame)) - sdl_name = bo.plasma_modifiers.animation.obj_sdl_anim - if sdl_name: - atcanim.globalVarName = sdl_name - elif sdl_name is None: - # Fixme? Not sure if we really need to expose this... - atcanim.easeInMin = 1.0 - atcanim.easeInMax = 1.0 - atcanim.easeInLength = 1.0 - atcanim.easeOutMin = 1.0 - atcanim.easeOutMax = 1.0 - atcanim.easeOutLength = 1.0 - def _convert_camera_animation(self, bo, so, obj_fcurves, data_fcurves): if data_fcurves: # The hard part about this crap is that FOV animations are not stored in ATC Animations diff --git a/korman/properties/modifiers/anim.py b/korman/properties/modifiers/anim.py index f59b853..5625b2d 100644 --- a/korman/properties/modifiers/anim.py +++ b/korman/properties/modifiers/anim.py @@ -64,11 +64,15 @@ class PlasmaAnimationModifier(ActionModifier, PlasmaModifierProperties): description="Name of the SDL variable to use for this animation", options=set()) + @property + def anim_type(self): + return plAgeGlobalAnim if self.obj_sdl_anim else plATCAnim + def export(self, exporter, bo, so): action = self.blender_action - - anim_data = bo.plasma_modifiers.animation - if anim_data.obj_sdl_anim: + anim_mod = bo.plasma_modifiers.animation + anim = exporter.mgr.find_create_object(anim_mod.anim_type, so=so) + if isinstance(anim, plAgeGlobalAnim): atcanim = exporter.mgr.find_create_object(plAgeGlobalAnim, so=so) else: atcanim = exporter.mgr.find_create_object(plATCAnim, so=so) From b9db5476696ff537290982586872b8a4d4b5f5a7 Mon Sep 17 00:00:00 2001 From: Patrick Dulebohn Date: Fri, 9 Oct 2020 18:16:24 -0400 Subject: [PATCH 28/50] Even more added fixes More Hoikas suggestions that should get things ready for review. --- korman/exporter/animation.py | 40 ++++++++++++----------------- korman/properties/modifiers/anim.py | 6 ++--- korman/ui/modifiers/anim.py | 10 +++++--- 3 files changed, 25 insertions(+), 31 deletions(-) diff --git a/korman/exporter/animation.py b/korman/exporter/animation.py index 4d06a45..623bd03 100644 --- a/korman/exporter/animation.py +++ b/korman/exporter/animation.py @@ -73,10 +73,7 @@ class AnimationConverter: # 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) anim_mod = bo.plasma_modifiers.animation - if anim_mod.obj_sdl_anim: - atcanim = self._mgr.find_create_object(plAgeGlobalAnim, so=so) - else: - atcanim = self._mgr.find_create_object(plATCAnim, so=so) + atcanim = self._mgr.find_create_object(anim_mod.anim_type, so=so) # Add the animation data to the ATC for i in applicators: @@ -93,26 +90,23 @@ class AnimationConverter: if i is not None: yield i.frame_range[index] atcanim.name = "(Entire Animation)" - def anim_range_type(plAGAnim): - if anim_mod.obj_sdl_anim: - atcanim.start = self._convert_frame_time(min(get_ranges(obj_action, data_action, index=0))) - atcanim.end = self._convert_frame_time(max(get_ranges(obj_action, data_action, index=1))) - atcanim.globalVarName = anim_mod.obj_sdl_anim - else: - 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))) - # Fixme? Not sure if we really need to expose this... - atcanim.easeInMin = 1.0 - atcanim.easeInMax = 1.0 - atcanim.easeInLength = 1.0 - atcanim.easeOutMin = 1.0 - atcanim.easeOutMax = 1.0 - atcanim.easeOutLength = 1.0 - + sdl_name = bo.plasma_modifiers.animation.obj_sdl_anim + atcanim.start = self._convert_frame_time(min(get_ranges(obj_action, data_action, index=0))) + atcanim.end = self._convert_frame_time(max(get_ranges(obj_action, data_action, index=1))) + if isinstance(atcanim, plAgeGlobalAnim): + atcanim.globalVarName = anim_mod.obj_sdl_anim + if isinstance(atcanim, plATCAnim): # Marker points - if obj_action is not None and anim_mod is None: - for marker in obj_action.pose_markers: - atcanim.setMarker(marker.name, self._convert_frame_time(marker.frame)) + if obj_action is not None and not anim_data.obj_sdl_anim: + for marker in obj_action.pose_markers: + atcanim.setMarker(marker.name, self._convert_frame_time(marker.frame)) + # Fixme? Not sure if we really need to expose this... + atcanim.easeInMin = 1.0 + atcanim.easeInMax = 1.0 + atcanim.easeInLength = 1.0 + atcanim.easeOutMin = 1.0 + atcanim.easeOutMax = 1.0 + atcanim.easeOutLength = 1.0 def _convert_camera_animation(self, bo, so, obj_fcurves, data_fcurves): if data_fcurves: diff --git a/korman/properties/modifiers/anim.py b/korman/properties/modifiers/anim.py index 5625b2d..1bcd989 100644 --- a/korman/properties/modifiers/anim.py +++ b/korman/properties/modifiers/anim.py @@ -71,10 +71,8 @@ class PlasmaAnimationModifier(ActionModifier, PlasmaModifierProperties): def export(self, exporter, bo, so): action = self.blender_action anim_mod = bo.plasma_modifiers.animation - anim = exporter.mgr.find_create_object(anim_mod.anim_type, so=so) - if isinstance(anim, plAgeGlobalAnim): - atcanim = exporter.mgr.find_create_object(plAgeGlobalAnim, so=so) - else: + atcanim = exporter.mgr.find_create_object(anim_mod.anim_type, so=so) + if not isinstance(atcanim, plAgeGlobalAnim): atcanim = exporter.mgr.find_create_object(plATCAnim, so=so) atcanim.autoStart = self.auto_start atcanim.loop = self.loop diff --git a/korman/ui/modifiers/anim.py b/korman/ui/modifiers/anim.py index 5decd6d..2903e65 100644 --- a/korman/ui/modifiers/anim.py +++ b/korman/ui/modifiers/anim.py @@ -32,6 +32,7 @@ def animation(modifier, layout, context): return split = layout.split() + col = layout.column() col = split.column() col.prop(modifier, "auto_start") col = split.column() @@ -40,12 +41,13 @@ def animation(modifier, layout, context): if action: layout.prop_search(modifier, "initial_marker", action, "pose_markers", icon="PMARKER") col = layout.column() - col.enabled = modifier.loop + col.enabled = modifier.loop and not modifier.obj_sdl_anim col.prop_search(modifier, "loop_start", action, "pose_markers", icon="PMARKER") col.prop_search(modifier, "loop_end", action, "pose_markers", icon="PMARKER") - - col.label("SDL Animation:") - col.prop(modifier, "obj_sdl_anim", text="") + col = layout.column() + col.label("SDL Animation:") + col.prop(modifier, "obj_sdl_anim", text="") + def animation_filter(modifier, layout, context): split = layout.split() From 891b8f3fca7cee7811355d41a47412b58c12bf11 Mon Sep 17 00:00:00 2001 From: Patrick Dulebohn Date: Fri, 9 Oct 2020 20:13:00 -0400 Subject: [PATCH 29/50] More adjustments and fixes Another batch of fixes and slight UI adjustment per Hoikas. --- korman/exporter/animation.py | 6 +++--- korman/properties/modifiers/anim.py | 1 - korman/ui/modifiers/anim.py | 7 ++----- 3 files changed, 5 insertions(+), 9 deletions(-) diff --git a/korman/exporter/animation.py b/korman/exporter/animation.py index 623bd03..054375a 100644 --- a/korman/exporter/animation.py +++ b/korman/exporter/animation.py @@ -90,14 +90,14 @@ class AnimationConverter: if i is not None: yield i.frame_range[index] atcanim.name = "(Entire Animation)" - sdl_name = bo.plasma_modifiers.animation.obj_sdl_anim + sdl_name = anim_mod.obj_sdl_anim atcanim.start = self._convert_frame_time(min(get_ranges(obj_action, data_action, index=0))) atcanim.end = self._convert_frame_time(max(get_ranges(obj_action, data_action, index=1))) if isinstance(atcanim, plAgeGlobalAnim): atcanim.globalVarName = anim_mod.obj_sdl_anim if isinstance(atcanim, plATCAnim): - # Marker points - if obj_action is not None and not anim_data.obj_sdl_anim: + # Marker points + if obj_action is not None: for marker in obj_action.pose_markers: atcanim.setMarker(marker.name, self._convert_frame_time(marker.frame)) # Fixme? Not sure if we really need to expose this... diff --git a/korman/properties/modifiers/anim.py b/korman/properties/modifiers/anim.py index 1bcd989..3f6c4f0 100644 --- a/korman/properties/modifiers/anim.py +++ b/korman/properties/modifiers/anim.py @@ -73,7 +73,6 @@ class PlasmaAnimationModifier(ActionModifier, PlasmaModifierProperties): anim_mod = bo.plasma_modifiers.animation atcanim = exporter.mgr.find_create_object(anim_mod.anim_type, so=so) if not isinstance(atcanim, plAgeGlobalAnim): - atcanim = exporter.mgr.find_create_object(plATCAnim, so=so) atcanim.autoStart = self.auto_start atcanim.loop = self.loop diff --git a/korman/ui/modifiers/anim.py b/korman/ui/modifiers/anim.py index 2903e65..b848ac2 100644 --- a/korman/ui/modifiers/anim.py +++ b/korman/ui/modifiers/anim.py @@ -32,7 +32,6 @@ def animation(modifier, layout, context): return split = layout.split() - col = layout.column() col = split.column() col.prop(modifier, "auto_start") col = split.column() @@ -44,11 +43,9 @@ def animation(modifier, layout, context): col.enabled = modifier.loop and not modifier.obj_sdl_anim col.prop_search(modifier, "loop_start", action, "pose_markers", icon="PMARKER") col.prop_search(modifier, "loop_end", action, "pose_markers", icon="PMARKER") - col = layout.column() - col.label("SDL Animation:") - col.prop(modifier, "obj_sdl_anim", text="") + layout.separator() + layout.prop(modifier, "obj_sdl_anim") - def animation_filter(modifier, layout, context): split = layout.split() From 2f6fa7a75dfa32257ccf15f04f22ca53c6a17a94 Mon Sep 17 00:00:00 2001 From: Patrick Dulebohn Date: Mon, 12 Oct 2020 20:03:47 -0400 Subject: [PATCH 30/50] Fix SDL state for animated lamps Slight indent change to enable SDL states for lamp RGB and energy animations. --- korman/ui/modifiers/anim.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/korman/ui/modifiers/anim.py b/korman/ui/modifiers/anim.py index b848ac2..614ff07 100644 --- a/korman/ui/modifiers/anim.py +++ b/korman/ui/modifiers/anim.py @@ -43,8 +43,8 @@ def animation(modifier, layout, context): col.enabled = modifier.loop and not modifier.obj_sdl_anim col.prop_search(modifier, "loop_start", action, "pose_markers", icon="PMARKER") col.prop_search(modifier, "loop_end", action, "pose_markers", icon="PMARKER") - layout.separator() - layout.prop(modifier, "obj_sdl_anim") + layout.separator() + layout.prop(modifier, "obj_sdl_anim") def animation_filter(modifier, layout, context): split = layout.split() From 09e7418e72bc3c9367fa7a74e930d4c7b0e75f6d Mon Sep 17 00:00:00 2001 From: Patrick Dulebohn Date: Wed, 14 Oct 2020 17:30:36 -0400 Subject: [PATCH 31/50] Fix volume conversion Keyframes need negative values in order for sound volume animations to work properly, and this change, per Hoikas, will fix this. --- korman/exporter/animation.py | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/korman/exporter/animation.py b/korman/exporter/animation.py index 6740da9..9f8a608 100644 --- a/korman/exporter/animation.py +++ b/korman/exporter/animation.py @@ -293,11 +293,7 @@ class AnimationConverter: if not fcurves: return None - def convert_volume(value): - if value == 0.0: - return 0.0 - else: - return math.log10(value) * 20.0 + convert_volume = lambda x: math.log10(max(.01, x / 100.0)) * 20.0 for sound in soundemit.sounds: path = "{}.volume".format(sound.path_from_id()) From 4c37ad41fa27378e34c1003debda49fe905461d5 Mon Sep 17 00:00:00 2001 From: Patrick Dulebohn Date: Tue, 20 Oct 2020 15:46:26 -0400 Subject: [PATCH 32/50] Fix Exponential Fog For some reason, SetDefExp2 type fog doesn't seem to work upon export, but regular SetDefExp does. This is just a proposed minor change to use the regular SetDefExp option if fixing the other is too big a task. --- korman/exporter/manager.py | 4 ++-- korman/properties/prop_world.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/korman/exporter/manager.py b/korman/exporter/manager.py index f62672c..034620a 100644 --- a/korman/exporter/manager.py +++ b/korman/exporter/manager.py @@ -321,8 +321,8 @@ class ExportManager: stream.writeLine("Graphics.Renderer.Fog.SetDefColor {} {} {}".format(*fni.fog_color)) if fni.fog_method == "linear": stream.writeLine("Graphics.Renderer.Fog.SetDefLinear {} {} {}".format(fni.fog_start, fni.fog_end, fni.fog_density)) - elif fni.fog_method == "exp2": - stream.writeLine("Graphics.Renderer.Fog.SetDefExp2 {} {}".format(fni.fog_end, fni.fog_density)) + elif fni.fog_method == "exp": + stream.writeLine("Graphics.Renderer.Fog.SetDefExp {} {}".format(fni.fog_end, fni.fog_density)) def _write_pages(self): age_name = self._age_info.name diff --git a/korman/properties/prop_world.py b/korman/properties/prop_world.py index 1e159a5..453f2d9 100644 --- a/korman/properties/prop_world.py +++ b/korman/properties/prop_world.py @@ -31,7 +31,7 @@ class PlasmaFni(bpy.types.PropertyGroup): fog_method = EnumProperty(name="Fog Type", items=[ ("linear", "Linear", "Linear Fog"), - ("exp2", "Exponential", "Exponential Fog"), + ("exp", "Exponential", "Exponential Fog"), ("none", "None", "Use fog from the previous age") ]) fog_start = FloatProperty(name="Start", From cf217d352881c96be040d61643ea37225b94c0c7 Mon Sep 17 00:00:00 2001 From: Patrick Dulebohn Date: Mon, 26 Oct 2020 23:52:07 -0400 Subject: [PATCH 33/50] Add Linear Fog Value Warning Warns the user if the linear fog start value is greater than the end value, which can cause visual fog problems. --- korman/ui/ui_world.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/korman/ui/ui_world.py b/korman/ui/ui_world.py index 5443248..c4bcb8c 100644 --- a/korman/ui/ui_world.py +++ b/korman/ui/ui_world.py @@ -262,6 +262,10 @@ class PlasmaEnvironmentPanel(AgeButtonsPanel, bpy.types.Panel): layout = self.layout fni = context.world.plasma_fni + # warn about reversed linear fog values + if fni.fog_method == "linear" and fni.fog_start >= fni.fog_end and (fni.fog_start + fni.fog_end) != 0: + layout.label(text="Fog Start Value should be less than the End Value", icon="ERROR") + # basic colors split = layout.split() col = split.column() From 39a3a88bb828f7e14ecc2a3039c285ddbe86688c Mon Sep 17 00:00:00 2001 From: Adam Johnson Date: Wed, 6 Jan 2021 10:27:28 -0500 Subject: [PATCH 34/50] Ensure PFM-related lights are marked animated. --- korman/exporter/manager.py | 8 ++++++ korman/nodes/node_python.py | 52 +++++++++++++++++++++++++------------ 2 files changed, 43 insertions(+), 17 deletions(-) diff --git a/korman/exporter/manager.py b/korman/exporter/manager.py index f62672c..859420a 100644 --- a/korman/exporter/manager.py +++ b/korman/exporter/manager.py @@ -16,6 +16,7 @@ import bpy from pathlib import Path from PyHSPlasma import * +from typing import Iterable import weakref from . import explosions @@ -190,6 +191,13 @@ class ExportManager: else: return plEncryptedStream.kEncXtea + def find_interfaces(self, pClass, so : plSceneObject) -> Iterable[plObjInterface]: + assert issubclass(pClass, plObjInterface) + + for i in (i.object for i in so.interfaces): + if isinstance(i, pClass): + yield i + def find_create_key(self, pClass, bl=None, name=None, so=None): key = self.find_key(pClass, bl, name, so) if key is None: diff --git a/korman/nodes/node_python.py b/korman/nodes/node_python.py index 9b9dcbc..c758b3e 100644 --- a/korman/nodes/node_python.py +++ b/korman/nodes/node_python.py @@ -256,6 +256,10 @@ class PlasmaPythonFileNode(PlasmaVersionedNode, bpy.types.Node): def export(self, exporter, bo, so): pfm = self.get_key(exporter, so).object + # Special PFM-SO handling ahoy - be sure to do it for all objects this PFM is attached to. + # Otherwise, you get non-determinant behavior. + self._export_ancillary_sceneobject(exporter, so) + # No need to continue if the PFM was already generated. if pfm.filename: return @@ -276,7 +280,6 @@ class PlasmaPythonFileNode(PlasmaVersionedNode, bpy.types.Node): # Handle exporting the Python Parameters attrib_sockets = (i for i in self.inputs if i.is_linked) for socket in attrib_sockets: - attrib = socket.attribute_type from_node = socket.links[0].from_node value = from_node.value if socket.is_simple_value else from_node.get_key(exporter, so) @@ -285,27 +288,42 @@ class PlasmaPythonFileNode(PlasmaVersionedNode, bpy.types.Node): for i in value: param = plPythonParameter() param.id = socket.attribute_id - param.valueType = _attrib2param[attrib] + param.valueType = _attrib2param[socket.attribute_type] param.value = i - # Key type sanity checking... Because I trust no user. if not socket.is_simple_value: - if i is None: - msg = "'{}' Node '{}' didn't return a key and therefore will be unavailable to Python".format( - self.id_data.name, from_node.name) - exporter.report.warn(msg, indent=3) - else: - key_type = _attrib_key_types[attrib] - if isinstance(key_type, tuple): - good_key = i.type in key_type - else: - good_key = i.type == key_type - if not good_key: - msg = "'{}' Node '{}' returned an unexpected key type '{}'".format( - self.id_data.name, from_node.name, plFactory.ClassName(i.type)) - exporter.report.warn(msg, indent=3) + self._export_key_attrib(exporter, bo, so, i, socket) pfm.addParameter(param) + def _export_ancillary_sceneobject(self, exporter, so : plSceneObject) -> None: + # Danger: Special case evil ahoy... + # If the key is an object that represents a lamp, we have to assume that the reason it's + # being passed to Python is so it can be turned on/off at will. That means it's technically + # an animated lamp. + for light in exporter.mgr.find_interfaces(plLightInfo, so): + exporter.report.msg("Marking RT light '{}' as animated due to usage in a Python File node", + so.key.name, indent=3) + light.setProperty(plLightInfo.kLPMovable, True) + + def _export_key_attrib(self, exporter, bo, so : plSceneObject, key : plKey, socket) -> None: + if key is None: + exporter.report.warn("Attribute '{}' didn't return a key and therefore will be unavailable to Python", + self.id_data.name, socket.links[0].name, indent=3) + return + + key_type = _attrib_key_types[socket.attribute_type] + if isinstance(key_type, tuple): + good_key = key.type in key_type + else: + good_key = key.type == key_type + if not good_key: + exporter.report.warn("'{}' Node '{}' returned an unexpected key type '{}'", + self.id_data.name, socket.links[0].from_node.name, + plFactory.ClassName(key.type), indent=3) + + if isinstance(key.object, plSceneObject): + self._export_ancillary_sceneobject(exporter, key.object) + def _get_attrib_sockets(self, idx): for i in self.inputs: if i.attribute_id == idx: From 172d8083aa81470c47dd0bc8249889a088c2d045 Mon Sep 17 00:00:00 2001 From: Adam Johnson Date: Wed, 6 Jan 2021 16:53:47 -0500 Subject: [PATCH 35/50] Only set runtime colors to black if we are not preshading. Otherwise, what we wind up doing is making it pretty much impossible for RT lights to affect objects with kLiteMaterial. --- korman/exporter/material.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/korman/exporter/material.py b/korman/exporter/material.py index d2a2ba9..3013ea6 100644 --- a/korman/exporter/material.py +++ b/korman/exporter/material.py @@ -1184,7 +1184,7 @@ class MaterialConverter: return utils.color(color) def get_material_runtime(self, bo, bm, color=None) -> hsColorRGBA: - if not bo.plasma_modifiers.lighting.rt_lights: + if not bo.plasma_modifiers.lighting.preshade: return hsColorRGBA.kBlack if color is None: color = bm.diffuse_color From 7d82db31855810325bf2d7f5fea68f9f059d4489 Mon Sep 17 00:00:00 2001 From: Patrick Dulebohn Date: Fri, 5 Feb 2021 11:31:00 -0500 Subject: [PATCH 36/50] Wind Object fix A long time coming but only now just fixed, but this makes a small correction to the wind object part of the waveset modifier to get it to work. --- korman/properties/modifiers/water.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/korman/properties/modifiers/water.py b/korman/properties/modifiers/water.py index eafa22f..806883b 100644 --- a/korman/properties/modifiers/water.py +++ b/korman/properties/modifiers/water.py @@ -249,7 +249,7 @@ class PlasmaWaterModifier(idprops.IDPropMixin, PlasmaModifierProperties, bpy.typ # This is much like what happened in PyPRP speed = self.wind_speed - matrix = wind_obj.matrix_world + matrix = self.wind_object.matrix_world wind_dir = hsVector3(matrix[1][0] * speed, matrix[1][1] * speed, matrix[1][2] * speed) else: # Stolen shamelessly from PyPRP From a0022ef067fb63ce63c79903ddca313b469522ca Mon Sep 17 00:00:00 2001 From: Adam Johnson Date: Wed, 10 Feb 2021 21:42:44 -0500 Subject: [PATCH 37/50] Fix #227. --- korman/exporter/convert.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/korman/exporter/convert.py b/korman/exporter/convert.py index a1d15e7..55cb30a 100644 --- a/korman/exporter/convert.py +++ b/korman/exporter/convert.py @@ -431,7 +431,7 @@ class Exporter: if not valid_path: filepath = bpy.context.blend_data.filepath if not filepath: - filepath = self.filepath + filepath = self._op.filepath filepath = str(Path(filepath).with_suffix(".ktc")) age.texcache_path = filepath return filepath From 5426643eeca2053a0aa482a8d61f3a8a78d4e9b7 Mon Sep 17 00:00:00 2001 From: Adam Johnson Date: Thu, 11 Feb 2021 15:18:08 -0500 Subject: [PATCH 38/50] Round funny values to hide float precision details. --- korman/exporter/manager.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/korman/exporter/manager.py b/korman/exporter/manager.py index 2599673..f2f2d26 100644 --- a/korman/exporter/manager.py +++ b/korman/exporter/manager.py @@ -321,16 +321,16 @@ class ExportManager: with output.generate_dat_file(f, enc=self._encryption) as stream: fni = bpy.context.scene.world.plasma_fni - stream.writeLine("Graphics.Renderer.SetClearColor {} {} {}".format(*fni.clear_color)) - stream.writeLine("Graphics.Renderer.SetYon {}".format(fni.yon)) + stream.writeLine("Graphics.Renderer.SetClearColor {.2f} {.2f} {.2f}".format(*fni.clear_color)) + stream.writeLine("Graphics.Renderer.SetYon {.1f}".format(fni.yon)) if fni.fog_method == "none": stream.writeLine("Graphics.Renderer.Fog.SetDefLinear 0 0 0") else: - stream.writeLine("Graphics.Renderer.Fog.SetDefColor {} {} {}".format(*fni.fog_color)) + stream.writeLine("Graphics.Renderer.Fog.SetDefColor {.2f} {.2f} {.2f}".format(*fni.fog_color)) if fni.fog_method == "linear": - stream.writeLine("Graphics.Renderer.Fog.SetDefLinear {} {} {}".format(fni.fog_start, fni.fog_end, fni.fog_density)) + stream.writeLine("Graphics.Renderer.Fog.SetDefLinear {.2f} {.2f} {.2f}".format(fni.fog_start, fni.fog_end, fni.fog_density)) elif fni.fog_method == "exp": - stream.writeLine("Graphics.Renderer.Fog.SetDefExp {} {}".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): age_name = self._age_info.name From 64c59480e5c797f6b4fc8a6f85d87c5a5d5d48c8 Mon Sep 17 00:00:00 2001 From: Adam Johnson Date: Tue, 16 Feb 2021 19:47:45 -0500 Subject: [PATCH 39/50] Who reviewed this? --- korman/exporter/manager.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/korman/exporter/manager.py b/korman/exporter/manager.py index f2f2d26..ad6b7da 100644 --- a/korman/exporter/manager.py +++ b/korman/exporter/manager.py @@ -321,16 +321,16 @@ class ExportManager: with output.generate_dat_file(f, enc=self._encryption) as stream: fni = bpy.context.scene.world.plasma_fni - stream.writeLine("Graphics.Renderer.SetClearColor {.2f} {.2f} {.2f}".format(*fni.clear_color)) - stream.writeLine("Graphics.Renderer.SetYon {.1f}".format(fni.yon)) + stream.writeLine("Graphics.Renderer.SetClearColor {:.2f} {:.2f} {:.2f}".format(*fni.clear_color)) + stream.writeLine("Graphics.Renderer.SetYon {:.1f}".format(fni.yon)) if fni.fog_method == "none": stream.writeLine("Graphics.Renderer.Fog.SetDefLinear 0 0 0") else: - stream.writeLine("Graphics.Renderer.Fog.SetDefColor {.2f} {.2f} {.2f}".format(*fni.fog_color)) + stream.writeLine("Graphics.Renderer.Fog.SetDefColor {:.2f} {:.2f} {:.2f}".format(*fni.fog_color)) if fni.fog_method == "linear": - stream.writeLine("Graphics.Renderer.Fog.SetDefLinear {.2f} {.2f} {.2f}".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 == "exp": - stream.writeLine("Graphics.Renderer.Fog.SetDefExp {.2f} {.2f}".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): age_name = self._age_info.name From 419a6f396e8c0d0b18d95c98f2e1c8058bfa6691 Mon Sep 17 00:00:00 2001 From: Adam Johnson Date: Tue, 16 Feb 2021 20:03:32 -0500 Subject: [PATCH 40/50] Fix incorrect FPS assumption with layer animations. This was troublesome in that the Blender documentation implies that `FCurve.range()` returns times when it actually returns frame numbers. This superceeds and closes #216. Prefer this fix because `functools.reduce` is not as readable and the formatting is closer to what exists in the animation converter. --- korman/exporter/material.py | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/korman/exporter/material.py b/korman/exporter/material.py index 3013ea6..5cc3fa3 100644 --- a/korman/exporter/material.py +++ b/korman/exporter/material.py @@ -534,12 +534,10 @@ class MaterialConverter: atc = layer_animation.timeConvert # Since we are harvesting from the material action but are exporting to a layer, the - # action's range is relatively useless. We'll figure our own. - start, end = functools.reduce(lambda x, y: (min(x[0], y[0]), max(x[1], y[1])), - (fcurve.range() for fcurve in fcurves)) - - atc.begin = start / fps - atc.end = end / fps + # action's range is relatively useless. We'll figure our own. Reminder: the blender + # documentation is wrong -- FCurve.range() returns a sequence of frame numbers, not times. + atc.begin = min((fcurve.range()[0] for fcurve in fcurves)) * (30.0 / fps) / fps + atc.end = max((fcurve.range()[1] for fcurve in fcurves)) * (30.0 / fps) / fps layer_props = tex_slot.texture.plasma_layer if not layer_props.anim_auto_start: From 2cff0d94644eaef3e76c17c1a61f7a858529a1c4 Mon Sep 17 00:00:00 2001 From: Adam Johnson Date: Tue, 16 Feb 2021 20:05:24 -0500 Subject: [PATCH 41/50] Don't encrypt any MOUL files at all. This should be done by a build tool, eg UruManifest --- korman/exporter/outfile.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/korman/exporter/outfile.py b/korman/exporter/outfile.py index 8e71174..d3ee7e6 100644 --- a/korman/exporter/outfile.py +++ b/korman/exporter/outfile.py @@ -249,7 +249,8 @@ class OutputFiles: backing_stream = stream # No sense in wasting time encrypting data that isn't going to be used in the export - if not bogus: + # Also, don't encrypt any MOUL files at all. + if not bogus and self._version != pvMoul: enc = kwargs.get("enc", None) if enc is not None: stream = plEncryptedStream(self._version) From f7499f25bb0634386206d4feba76235217cfdb01 Mon Sep 17 00:00:00 2001 From: Adam Johnson Date: Tue, 16 Feb 2021 20:37:42 -0500 Subject: [PATCH 42/50] Add some age detail validation nastygrams. Fixes #234. --- korman/properties/prop_world.py | 15 +++++++++++++++ korman/ui/ui_world.py | 19 ++++++++++++++++--- 2 files changed, 31 insertions(+), 3 deletions(-) diff --git a/korman/properties/prop_world.py b/korman/properties/prop_world.py index 453f2d9..5e223af 100644 --- a/korman/properties/prop_world.py +++ b/korman/properties/prop_world.py @@ -148,6 +148,14 @@ class PlasmaPage(bpy.types.PropertyGroup): class PlasmaAge(bpy.types.PropertyGroup): def export(self, exporter): + if exporter.mgr.getVer() == pvMoul: + log_func = exporter.report.warn + else: + log_func = exporter.report.port + if self.seq_prefix <= self.MOUL_PREFIX_RANGE[0] or self.seq_prefix >= self.MOUL_PREFIX_RANGE[1]: + log_func("Age Sequence Prefix {} is potentially out of range (should be between {} and {})", + self.seq_prefix, *self.MOUL_PREFIX_RANGE) + _age_info = plAgeInfo() _age_info.dayLength = self.day_length _age_info.lingerTime = 180 # this is fairly standard @@ -157,6 +165,10 @@ class PlasmaAge(bpy.types.PropertyGroup): _age_info.startDateTime = self.start_time return _age_info + # Sequence prefix helpers + MOUL_PREFIX_RANGE = ((pow(2, 16) - pow(2, 15)) * -1, pow(2, 15) - 1) + SP_PRFIX_RANGE = ((pow(2, 24) - pow(2, 23)) * -1, pow(2, 23) - 1) + day_length = FloatProperty(name="Day Length", description="Length of a day (in hours) on this age", default=30.230000, @@ -169,7 +181,10 @@ class PlasmaAge(bpy.types.PropertyGroup): min=0) seq_prefix = IntProperty(name="Sequence Prefix", description="A unique numerical ID for this age", + min=SP_PRFIX_RANGE[0], soft_min=0, # Negative indicates global--advanced users only + soft_max=MOUL_PREFIX_RANGE[1], + max=SP_PRFIX_RANGE[1], default=100) pages = CollectionProperty(name="Pages", description="Registry pages for this age", diff --git a/korman/ui/ui_world.py b/korman/ui/ui_world.py index c4bcb8c..3ed422e 100644 --- a/korman/ui/ui_world.py +++ b/korman/ui/ui_world.py @@ -210,6 +210,8 @@ class PlasmaAgePanel(AgeButtonsPanel, bpy.types.Panel): # Age Names should really be legal Python 2.x identifiers for AgeSDLHooks legal_identifier = korlib.is_legal_python2_identifier(age.age_name) + illegal_age_name = not legal_identifier or '_' in age.age_name + bad_prefix = age.seq_prefix >= age.MOUL_PREFIX_RANGE[1] or age.seq_prefix <= age.MOUL_PREFIX_RANGE[0] # Core settings layout.separator() @@ -222,17 +224,28 @@ class PlasmaAgePanel(AgeButtonsPanel, bpy.types.Panel): col = split.column() col.label("Age Settings:") + col.alert = bad_prefix col.prop(age, "seq_prefix", text="ID") - col.alert = not legal_identifier or '_' in age.age_name + col.alert = illegal_age_name col.prop(age, "age_name", text="") + if age.seq_prefix >= age.MOUL_PREFIX_RANGE[1]: + layout.label(text="Your sequence prefix is too high for Myst Online: Uru Live", icon="ERROR") + elif age.seq_prefix <= age.MOUL_PREFIX_RANGE[0]: + # Unlikely. + layout.label(text="Your sequence prefix is too low for Myst Online: Uru Live", icon="ERROR") + # Display a hint if the identifier is illegal - if not legal_identifier: - if korlib.is_python_keyword(age.age_name): + if illegal_age_name: + if not age.age_name: + layout.label(text="Age names cannot be empty", icon="ERROR") + elif korlib.is_python_keyword(age.age_name): layout.label(text="Ages should not be named the same as a Python keyword", icon="ERROR") elif age.age_sdl: fixed_identifier = korlib.replace_python2_identifier(age.age_name) layout.label(text="Age's SDL will use the name '{}'".format(fixed_identifier), icon="ERROR") + if '_' in age.age_name: + layout.label(text="Age names should not contain underscores", icon="ERROR") layout.separator() split = layout.split() From 3b999f550d75a32ce53ab8de81377cb915561f46 Mon Sep 17 00:00:00 2001 From: Adam Johnson Date: Wed, 17 Feb 2021 15:09:35 -0500 Subject: [PATCH 43/50] Fix child font objects. Font objects were exported as a completely different SceneObject, causing the parenting relationship to be broken. This ensures that the drawable is properly attached to the correct SceneObject. --- korman/exporter/convert.py | 7 +++++-- korman/exporter/mesh.py | 4 ++-- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/korman/exporter/convert.py b/korman/exporter/convert.py index 55cb30a..5f2555d 100644 --- a/korman/exporter/convert.py +++ b/korman/exporter/convert.py @@ -287,14 +287,17 @@ class Exporter: def _export_mesh_blobj(self, so, bo): self.animation.convert_object_animations(bo, so) if bo.data.materials: - self.mesh.export_object(bo) + self.mesh.export_object(bo, so) else: self.report.msg("No material(s) on the ObData, so no drawables", indent=1) def _export_font_blobj(self, so, bo): self.animation.convert_object_animations(bo, so) with utils.temporary_mesh_object(bo) as meshObj: - self._export_mesh_blobj(so, meshObj) + if bo.data.materials: + self.mesh.export_object(meshObj, so) + else: + self.report.msg("No material(s) on the ObData, so no drawables", indent=1) def _export_referenced_node_trees(self): self.report.progress_advance() diff --git a/korman/exporter/mesh.py b/korman/exporter/mesh.py index e629395..e8f1ac7 100644 --- a/korman/exporter/mesh.py +++ b/korman/exporter/mesh.py @@ -575,7 +575,7 @@ class MeshConverter(_MeshManager): # Sequence of tuples (material_index, material) return sorted(((i, material_source[i]) for i in valid_materials), key=lambda x: x[0]) - def export_object(self, bo): + def export_object(self, bo, so : plSceneObject): # If this object has modifiers, then it's a unique mesh, and we don't need to try caching it # Otherwise, let's *try* to share meshes as best we can... if bo.modifiers: @@ -587,7 +587,7 @@ class MeshConverter(_MeshManager): # Create the DrawInterface if drawables: - diface = self._mgr.find_create_object(plDrawInterface, bl=bo) + diface = self._mgr.find_create_object(plDrawInterface, bl=bo, so=so) for dspan_key, idx in drawables: diface.addDrawable(dspan_key, idx) From 9a9bfa9ed67d4cf704b8e88da0aa56b216375b06 Mon Sep 17 00:00:00 2001 From: Adam Johnson Date: Wed, 17 Feb 2021 17:29:39 -0500 Subject: [PATCH 44/50] Fix child physicals appearing in the wrong location. A few observed problems: - In PotS, any physical parented to another object fell through the floor on link in. - In MOUL, physicals in a subworld were exported in the wrong coordinate system. They were exported in local-to-world space, but MOUL expects them in local-to-subworld space. - In PotS, any scaled physical parented to another object wiped out the visual (but not physical) scaling due to an improper flag setting. --- korman/exporter/physics.py | 55 +++++++++++++++++++++----------------- 1 file changed, 31 insertions(+), 24 deletions(-) diff --git a/korman/exporter/physics.py b/korman/exporter/physics.py index 4a4aa50..3806342 100644 --- a/korman/exporter/physics.py +++ b/korman/exporter/physics.py @@ -60,9 +60,7 @@ class PhysicsConverter: indices += (v[0], v[2], v[3],) return indices - def _convert_mesh_data(self, bo, physical, local_space, indices=True): - mat = bo.matrix_world - + def _convert_mesh_data(self, bo, physical, local_space, mat, indices=True): mesh = bo.to_mesh(bpy.context.scene, True, "RENDER", calc_tessface=False) with TemporaryObject(mesh, bpy.data.meshes.remove): if local_space: @@ -211,29 +209,39 @@ class PhysicsConverter: if tree_xformed: bo_xformed = bo.plasma_object.has_transform_animation + # Always pin these objects - otherwise they may start falling through the floor. + _set_phys_prop(plSimulationInterface.kPinned, simIface, physical) + # MOUL: only objects that have animation data are kPhysAnim if ver != pvMoul or bo_xformed: _set_phys_prop(plSimulationInterface.kPhysAnim, simIface, physical) - # PotS: objects inheriting parent animation only are not pinned - # MOUL: animated objects in subworlds are not pinned - if bo_xformed and (ver != pvMoul or subworld is None): - _set_phys_prop(plSimulationInterface.kPinned, simIface, physical) - # MOUL: child objects are kPassive - if ver == pvMoul and bo.parent is not None: - _set_phys_prop(plSimulationInterface.kPassive, simIface, physical) - # FilterCoordinateInterfaces are kPassive - if bo.plasma_object.ci_type == plFilterCoordInterface: + + # Any physical that is parented by not kickable (dynamic) is passive - + # meaning we don't need to report back any changes from physics. Same for + # plFilterCoordInterface, which filters out some axes. + if (bo.parent is not None and not mod.dynamic) or bo.plasma_object.ci_type == plFilterCoordInterface: _set_phys_prop(plSimulationInterface.kPassive, simIface, physical) # If the mass is zero, then we will fail to animate. Fix that. if physical.mass == 0.0: physical.mass = 1.0 + # Different Plasma versions have different ways they expect to get physical transforms. + # With Havok, massless objects are in absolute worldspace while massed (movable) objects + # are in object-local space. + # In PhysX, objects with a coordinate interface are in local to SUBWORLD space, otherwise + # they are in absolute worldspace. if ver <= pvPots: - local_space = physical.mass > 0.0 + local_space, mat = physical.mass > 0.0, bo.matrix_world + elif ver == pvMoul: + if self._exporter().has_coordiface(bo): + local_space = True + mat = subworld.matrix_world.inverted() * bo.matrix_world if subworld else bo.matrix_world + else: + local_space, mat = False, bo.matrix_world else: - local_space = self._exporter().has_coordiface(bo) - self._bounds_converters[bounds](bo, physical, local_space) + raise NotImplementedError("ODE physical transform") + self._bounds_converters[bounds](bo, physical, local_space, mat) else: simIface = so.sim.object physical = simIface.physical.object @@ -245,14 +253,14 @@ class PhysicsConverter: self._apply_props(simIface, physical, kwargs) - def _export_box(self, bo, physical, local_space): + def _export_box(self, bo, physical, local_space, mat): """Exports box bounds based on the object""" physical.boundsType = plSimDefs.kBoxBounds - vertices = self._convert_mesh_data(bo, physical, local_space, indices=False) + vertices = self._convert_mesh_data(bo, physical, local_space, mat, indices=False) physical.calcBoxBounds(vertices) - def _export_hull(self, bo, physical, local_space): + def _export_hull(self, bo, physical, local_space, mat): """Exports convex hull bounds based on the object""" physical.boundsType = plSimDefs.kHullBounds @@ -260,7 +268,6 @@ class PhysicsConverter: # bake them to convex hulls. Specifically, Windows 32-bit w/PhysX 2.6. Everything else just # needs to have us provide some friendlier data... with bmesh_from_object(bo) as mesh: - mat = bo.matrix_world if local_space: physical.pos = hsVector3(*mat.to_translation()) physical.rot = utils.quaternion(mat.to_quaternion()) @@ -273,24 +280,24 @@ class PhysicsConverter: verts = itertools.takewhile(lambda x: isinstance(x, BMVert), result["geom"]) physical.verts = [hsVector3(*i.co) for i in verts] - def _export_sphere(self, bo, physical, local_space): + def _export_sphere(self, bo, physical, local_space, mat): """Exports sphere bounds based on the object""" physical.boundsType = plSimDefs.kSphereBounds - vertices = self._convert_mesh_data(bo, physical, local_space, indices=False) + vertices = self._convert_mesh_data(bo, physical, local_space, mat, indices=False) physical.calcSphereBounds(vertices) - def _export_trimesh(self, bo, physical, local_space): + def _export_trimesh(self, bo, physical, local_space, mat): """Exports an object's mesh as exact physical bounds""" # 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) + vertices, indices = self._convert_mesh_data(mod.proxy_object, physical, local_space, mat) else: physical.boundsType = plSimDefs.kExplicitBounds - vertices, indices = self._convert_mesh_data(bo, physical, local_space) + vertices, indices = self._convert_mesh_data(bo, physical, local_space, mat) physical.verts = vertices physical.indices = indices From 59d852d3306e1169e8e75a3b4e7f963bc9551411 Mon Sep 17 00:00:00 2001 From: Adam Johnson Date: Wed, 17 Feb 2021 18:29:07 -0500 Subject: [PATCH 45/50] Fix kickables in subworlds. --- korman/exporter/physics.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/korman/exporter/physics.py b/korman/exporter/physics.py index 3806342..4692354 100644 --- a/korman/exporter/physics.py +++ b/korman/exporter/physics.py @@ -210,7 +210,9 @@ class PhysicsConverter: bo_xformed = bo.plasma_object.has_transform_animation # Always pin these objects - otherwise they may start falling through the floor. - _set_phys_prop(plSimulationInterface.kPinned, simIface, physical) + # Unless you've marked it kickable... + if not mod.dynamic: + _set_phys_prop(plSimulationInterface.kPinned, simIface, physical) # MOUL: only objects that have animation data are kPhysAnim if ver != pvMoul or bo_xformed: From ef589175367d3624352102b5eb0e635c85ed41dd Mon Sep 17 00:00:00 2001 From: Adam Johnson Date: Thu, 18 Feb 2021 18:34:08 -0500 Subject: [PATCH 46/50] Refactor keyframe handling for material diffuse anims. The old code was objectively terrible and placed the burden of handling missing values effectively on the user instead of just figuring it out. This is objectively better in that we can now count on all values being "known" at keyframe convert time. Whether that "known" is because it's a real keyframe, we evaluated it, or we pulled it out of our @$$ is another story, of course. --- korman/exporter/animation.py | 567 +++++++++++++++-------------------- 1 file changed, 241 insertions(+), 326 deletions(-) diff --git a/korman/exporter/animation.py b/korman/exporter/animation.py index 09c2663..f800f55 100644 --- a/korman/exporter/animation.py +++ b/korman/exporter/animation.py @@ -14,10 +14,13 @@ # along with Korman. If not, see . import bpy +from collections import defaultdict +import functools import itertools import math import mathutils from PyHSPlasma import * +from typing import * import weakref from . import utils @@ -27,10 +30,10 @@ class AnimationConverter: self._exporter = weakref.ref(exporter) self._bl_fps = bpy.context.scene.render.fps - def _convert_frame_time(self, frame_num): + def _convert_frame_time(self, frame_num : int) -> float: return frame_num / self._bl_fps - def convert_object_animations(self, bo, so): + def convert_object_animations(self, bo, so) -> None: if not bo.plasma_object.has_animation_data: return @@ -191,7 +194,7 @@ class AnimationConverter: return None energy_curve = next((i for i in fcurves if i.data_path == "energy" and i.keyframe_points), None) - color_curves = sorted((i for i in fcurves if i.data_path == "color" and i.keyframe_points), key=lambda x: x.array_index) + color_curves = [i for i in fcurves if i.data_path == "color" and i.keyframe_points] if energy_curve is None and color_curves is None: return None elif lamp.use_only_shadow: @@ -202,10 +205,15 @@ class AnimationConverter: return None # OK Specular is easy. We just toss out the color as a point3. - color_keyframes, color_bez = self._process_keyframes(color_curves, convert=lambda x: x * -1.0 if lamp.use_negative else None) + def convert_specular_animation(color): + if lamp.use_negative: + return map(lambda x: x * -1.0, color) + else: + return color + color_keyframes, color_bez = self._process_keyframes(color_curves, 3, lamp.color, convert_specular_animation) if color_keyframes and lamp.use_specular: channel = plPointControllerChannel() - channel.controller = self._make_point3_controller(color_curves, color_keyframes, color_bez, lamp.color) + channel.controller = self._make_point3_controller(color_keyframes, color_bez) applicator = plLightSpecularApplicator() applicator.channelName = name applicator.channel = channel @@ -214,18 +222,20 @@ class AnimationConverter: # Hey, look, it's a third way to process FCurves. YAY! def convert_diffuse_animation(color, energy): if lamp.use_negative: - return { key: (0.0 - value) * energy[0] for key, value in color.items() } + proc = lambda x: x * -1.0 * energy[0] else: - return { key: value * energy[0] for key, value in color.items() } - diffuse_defaults = { "color": lamp.color, "energy": lamp.energy } + proc = lambda x: x * energy[0] + return map(proc, color) + diffuse_channels = dict(color=3, energy=1) + diffuse_defaults = dict(color=lamp.color, energy=lamp.energy) diffuse_fcurves = color_curves + [energy_curve,] - diffuse_keyframes = self._process_fcurves(diffuse_fcurves, convert_diffuse_animation, diffuse_defaults) + diffuse_keyframes = self._process_fcurves(diffuse_fcurves, diffuse_channels, 3, convert_diffuse_animation, diffuse_defaults) if not diffuse_keyframes: return None # Whew. channel = plPointControllerChannel() - channel.controller = self._make_point3_controller([], diffuse_keyframes, False, []) + channel.controller = self._make_point3_controller(diffuse_keyframes, False) applicator = plLightDiffuseApplicator() applicator.channelName = name applicator.channel = channel @@ -239,8 +249,16 @@ class AnimationConverter: distance_fcurve = next((i for i in fcurves if i.data_path == "distance"), None) if energy_fcurve is None and distance_fcurve is None: return None - light_converter = self._exporter().light - intensity, atten_end = light_converter.convert_attenuation(lamp) + + light_converter, report = self._exporter().light, self._exporter().report + omni_fcurves = [distance_fcurve, energy_fcurve] + omni_channels = dict(distance=1, energy=1) + omni_defaults = dict(distance=lamp.distance, energy=lamp.energy) + + def convert_omni_atten(distance, energy): + intens = abs(energy[0]) + atten_end = distance[0] if lamp.use_sphere else distance[0] * 2 + return light_converter.convert_attenuation_linear(intens, atten_end) # All types allow animating cutoff if distance_fcurve is not None: @@ -255,15 +273,9 @@ class AnimationConverter: falloff = lamp.falloff_type if falloff == "CONSTANT": if energy_fcurve is not None: - self._exporter().report.warn("Constant attenuation cannot be animated in Plasma", ident=3) + report.warn("Constant attenuation cannot be animated in Plasma", ident=3) elif falloff == "INVERSE_LINEAR": - def convert_linear_atten(distance, energy): - intens = abs(energy[0]) - atten_end = distance[0] if lamp.use_sphere else distance[0] * 2 - return light_converter.convert_attenuation_linear(intens, atten_end) - - keyframes = self._process_fcurves([distance_fcurve, energy_fcurve], convert_linear_atten, - {"distance": lamp.distance, "energy": lamp.energy}) + keyframes = self._process_fcurves(omni_fcurves, omni_channels, 1, convert_omni_atten, omni_defaults) if keyframes: channel = plScalarControllerChannel() channel.controller = self._make_scalar_leaf_controller(keyframes, False) @@ -273,13 +285,8 @@ class AnimationConverter: yield applicator elif falloff == "INVERSE_SQUARE": if self._mgr.getVer() >= pvMoul: - def convert_quadratic_atten(distance, energy): - intens = abs(energy[0]) - atten_end = distance[0] if lamp.use_sphere else distance[0] * 2 - return light_converter.convert_attenuation_quadratic(intens, atten_end) - - keyframes = self._process_fcurves([distance_fcurve, energy_fcurve], convert_quadratic_atten, - {"distance": lamp.distance, "energy": lamp.energy}) + report.port("Lamp {} Falloff animations are only supported in Myst Online: Uru Live", falloff, indent=3) + keyframes = self._process_fcurves(omni_fcurves, omni_channels, 1, convert_omni_atten, omni_defaults) if keyframes: channel = plScalarControllerChannel() channel.controller = self._make_scalar_leaf_controller(keyframes, False) @@ -288,9 +295,9 @@ class AnimationConverter: applicator.channel = channel yield applicator else: - self._exporter().report.port("Lamp Falloff '{}' animations only partially supported for this version of Plasma", falloff, indent=3) + report.warn("Lamp {} Falloff animations are not supported for this version of Plasma", falloff, indent=3) else: - self._exporter().report.warn("Lamp Falloff '{}' animations are not supported".format(falloff), ident=3) + report.warn("Lamp Falloff '{}' animations are not supported", falloff, ident=3) def _convert_sound_volume_animation(self, name, fcurves, soundemit): if not fcurves: @@ -340,8 +347,11 @@ class AnimationConverter: size = spot_size[0] value = size - (blend * size) return math.degrees(value) - defaults = { "spot_blend": lamp.spot_blend, "spot_size": lamp.spot_size } - keyframes = self._process_fcurves([blend_fcurve, size_fcurve], convert_spot_inner, defaults) + + inner_fcurves = [blend_fcurve, size_fcurve] + inner_channels = dict(spot_blend=1, spot_size=1) + inner_defaults = dict(spot_blend=lamp.spot_blend, spot_size=lamp.spot_size) + keyframes = self._process_fcurves(inner_fcurves, inner_channels, 1, convert_spot_inner, inner_defaults) if keyframes: channel = plScalarControllerChannel() @@ -351,7 +361,7 @@ class AnimationConverter: applicator.channel = channel yield applicator - def _convert_transform_animation(self, name, fcurves, xform, allow_empty=False): + def _convert_transform_animation(self, name, fcurves, xform, allow_empty=False) -> Union[None, plMatrixChannelApplicator]: tm = self.convert_transform_controller(fcurves, xform, allow_empty) if tm is None and not allow_empty: return None @@ -366,13 +376,14 @@ class AnimationConverter: return applicator - def convert_transform_controller(self, fcurves, xform, allow_empty=False): + def convert_transform_controller(self, fcurves, xform, allow_empty=False) -> Union[None, plCompoundController]: if not fcurves and not allow_empty: return None - pos = self.make_pos_controller(fcurves, xform) - rot = self.make_rot_controller(fcurves, xform) - scale = self.make_scale_controller(fcurves, xform) + pos = self.make_pos_controller(fcurves, "location", xform.to_translation()) + # TODO: support rotation_quaternion + rot = self.make_rot_controller(fcurves, "rotation_euler", xform.to_euler()) + scale = self.make_scale_controller(fcurves, "scale", xform.to_scale()) if pos is None and rot is None and scale is None: if not allow_empty: return None @@ -383,17 +394,17 @@ class AnimationConverter: tm.Z = scale return tm - def get_anigraph_keys(self, bo=None, so=None): + def get_anigraph_keys(self, bo=None, so=None) -> Tuple[plKey, plKey]: mod = self._mgr.find_create_key(plAGModifier, so=so, bl=bo) master = self._mgr.find_create_key(plAGMasterMod, so=so, bl=bo) return mod, master - def get_anigraph_objects(self, bo=None, so=None): + def get_anigraph_objects(self, bo=None, so=None) -> Tuple[plAGModifier, plAGMasterMod]: mod = self._mgr.find_create_object(plAGModifier, so=so, bl=bo) master = self._mgr.find_create_object(plAGMasterMod, so=so, bl=bo) return mod, master - def get_animation_key(self, bo, so=None): + def get_animation_key(self, bo, so=None) -> plKey: # we might be controlling more than one animation. isn't that cute? # https://www.youtube.com/watch?v=hspNaoxzNbs # (but obviously this is not wrong...) @@ -403,72 +414,65 @@ class AnimationConverter: else: return self.get_anigraph_keys(bo, so)[1] - def make_matrix44_controller(self, fcurves, pos_path, scale_path, pos_default, scale_default): - def convert_matrix_keyframe(**kwargs): - pos = kwargs.get(pos_path) - scale = kwargs.get(scale_path) - - # Since only some position curves may be supplied, construct dict with all positions - allpos = dict(enumerate(pos_default)) - allscale = dict(enumerate(scale_default)) - allpos.update(pos) - allscale.update(scale) + def make_matrix44_controller(self, fcurves, pos_path : str, scale_path : str, pos_default, scale_default) -> Union[None, plLeafController]: + def convert_matrix_keyframe(**kwargs) -> hsMatrix44: + pos = kwargs[pos_path] + scale = kwargs[scale_path] matrix = hsMatrix44() - # Note: scale and pos are dicts, so we can't unpack - matrix.setTranslate(hsVector3(allpos[0], allpos[1], allpos[2])) - matrix.setScale(hsVector3(allscale[0], allscale[1], allscale[2])) + matrix.setTranslate(hsVector3(*pos)) + matrix.setScale(hsVector3(*scale)) return matrix fcurves = [i for i in fcurves if i.data_path == pos_path or i.data_path == scale_path] if not fcurves: return None + channels = { pos_path: 3, scale_path: 3 } default_values = { pos_path: pos_default, scale_path: scale_default } - keyframes = self._process_fcurves(fcurves, convert_matrix_keyframe, default_values) + keyframes = self._process_fcurves(fcurves, channels, 1, convert_matrix_keyframe, default_values) if not keyframes: return None # Now we make the controller return self._make_matrix44_controller(keyframes) - def make_pos_controller(self, fcurves, default_xform, convert=None): - pos_curves = [i for i in fcurves if i.data_path == "location" and i.keyframe_points] - keyframes, bez_chans = self._process_keyframes(pos_curves, convert) + def make_pos_controller(self, fcurves, data_path : str, default_xform, convert=None) -> Union[None, plLeafController]: + pos_curves = [i for i in fcurves if i.data_path == data_path and i.keyframe_points] + keyframes, bez_chans = self._process_keyframes(pos_curves, 3, default_xform, convert) if not keyframes: return None # At one point, I had some... insanity here to try to crush bezier channels and hand off to # blah blah blah... As it turns out, point3 keyframe's tangents are vector3s :) - ctrl = self._make_point3_controller(pos_curves, keyframes, bez_chans, default_xform.to_translation()) + ctrl = self._make_point3_controller(keyframes, bez_chans) return ctrl - def make_rot_controller(self, fcurves, default_xform, convert=None): - # TODO: support rotation_quaternion - rot_curves = [i for i in fcurves if i.data_path == "rotation_euler" and i.keyframe_points] - keyframes, bez_chans = self._process_keyframes(rot_curves, convert=None) + def make_rot_controller(self, fcurves, data_path : str, default_xform, convert=None) -> Union[None, plCompoundController, plLeafController]: + rot_curves = [i for i in fcurves if i.data_path == data_path and i.keyframe_points] + keyframes, bez_chans = self._process_keyframes(rot_curves, 3, default_xform, convert=None) if not keyframes: return None # Ugh. Unfortunately, it appears Blender's default interpolation is bezier. So who knows if # many users will actually see the benefit here? Makes me sad. if bez_chans: - ctrl = self._make_scalar_compound_controller(rot_curves, keyframes, bez_chans, default_xform.to_euler()) + ctrl = self._make_scalar_compound_controller(keyframes, bez_chans) else: - ctrl = self._make_quat_controller(rot_curves, keyframes, default_xform.to_euler()) + ctrl = self._make_quat_controller( keyframes) return ctrl - def make_scale_controller(self, fcurves, default_xform, convert=None): - scale_curves = [i for i in fcurves if i.data_path == "scale" and i.keyframe_points] - keyframes, bez_chans = self._process_keyframes(scale_curves, convert) + def make_scale_controller(self, fcurves, data_path : str, default_xform, convert=None) -> plLeafController: + scale_curves = [i for i in fcurves if i.data_path == data_path and i.keyframe_points] + keyframes, bez_chans = self._process_keyframes(scale_curves, 3, default_xform, convert) if not keyframes: return None # There is no such thing as a compound scale controller... in Plasma, anyway. - ctrl = self._make_scale_value_controller(scale_curves, keyframes, bez_chans, default_xform) + ctrl = self._make_scale_value_controller(keyframes, bez_chans) return ctrl - def make_scalar_leaf_controller(self, fcurve, convert=None): + def make_scalar_leaf_controller(self, fcurve, convert=None) -> Union[None, plLeafController]: keyframes, bezier = self._process_fcurve(fcurve, convert) if not keyframes: return None @@ -476,7 +480,7 @@ class AnimationConverter: ctrl = self._make_scalar_leaf_controller(keyframes, bezier) return ctrl - def _make_matrix44_controller(self, keyframes): + def _make_matrix44_controller(self, keyframes) -> plLeafController: ctrl = plLeafController() keyframe_type = hsKeyFrame.kMatrix44KeyFrame exported_frames = [] @@ -486,52 +490,32 @@ class AnimationConverter: exported.frame = keyframe.frame_num exported.frameTime = keyframe.frame_time exported.type = keyframe_type - exported.value = keyframe.value + exported.value = keyframe.values[0] exported_frames.append(exported) ctrl.keys = (exported_frames, keyframe_type) return ctrl - def _make_point3_controller(self, fcurves, keyframes, bezier, default_xform): + def _make_point3_controller(self, keyframes, bezier) -> plLeafController: ctrl = plLeafController() - subctrls = ("X", "Y", "Z") keyframe_type = hsKeyFrame.kBezPoint3KeyFrame if bezier else hsKeyFrame.kPoint3KeyFrame exported_frames = [] - ctrl_fcurves = { i.array_index: i for i in fcurves } for keyframe in keyframes: exported = hsPoint3Key() exported.frame = keyframe.frame_num exported.frameTime = keyframe.frame_time exported.type = keyframe_type - - in_tan = hsVector3() - out_tan = hsVector3() - value = hsVector3() - for i, subctrl in enumerate(subctrls): - fval = keyframe.values.get(i, None) - if fval is not None: - setattr(value, subctrl, fval) - setattr(in_tan, subctrl, keyframe.in_tans[i]) - setattr(out_tan, subctrl, keyframe.out_tans[i]) - else: - try: - setattr(value, subctrl, ctrl_fcurves[i].evaluate(keyframe.frame_num_blender)) - except KeyError: - setattr(value, subctrl, default_xform[i]) - setattr(in_tan, subctrl, 0.0) - setattr(out_tan, subctrl, 0.0) - exported.inTan = in_tan - exported.outTan = out_tan - exported.value = value + exported.inTan = hsVector3(*keyframe.in_tans) + exported.outTan = hsVector3(*keyframe.out_tans) + exported.value = hsVector3(*keyframe.values) exported_frames.append(exported) ctrl.keys = (exported_frames, keyframe_type) return ctrl - def _make_quat_controller(self, fcurves, keyframes, default_xform): + def _make_quat_controller(self, keyframes) -> plLeafController: ctrl = plLeafController() keyframe_type = hsKeyFrame.kQuatKeyFrame exported_frames = [] - ctrl_fcurves = { i.array_index: i for i in fcurves } for keyframe in keyframes: exported = hsQuatKey() @@ -540,58 +524,36 @@ class AnimationConverter: exported.type = keyframe_type # NOTE: quat keyframes don't do bezier nonsense - value = mathutils.Euler() - for i in range(3): - fval = keyframe.values.get(i, None) - if fval is not None: - value[i] = fval - else: - try: - value[i] = ctrl_fcurves[i].evaluate(keyframe.frame_num_blender) - except KeyError: - value[i] = default_xform[i] - quat = value.to_quaternion() - exported.value = utils.quaternion(quat) + value = mathutils.Euler(keyframe.values) + exported.value = utils.quaternion(value.to_quaternion()) exported_frames.append(exported) ctrl.keys = (exported_frames, keyframe_type) return ctrl - def _make_scalar_compound_controller(self, fcurves, keyframes, bez_chans, default_xform): + def _make_scalar_compound_controller(self, keyframes, bez_chans) -> plCompoundController: ctrl = plCompoundController() subctrls = ("X", "Y", "Z") for i in subctrls: setattr(ctrl, i, plLeafController()) exported_frames = ([], [], []) - ctrl_fcurves = { i.array_index: i for i in fcurves } for keyframe in keyframes: for i, subctrl in enumerate(subctrls): - fval = keyframe.values.get(i, None) - if fval is not None: - keyframe_type = hsKeyFrame.kBezScalarKeyFrame if i in bez_chans else hsKeyFrame.kScalarKeyFrame - exported = hsScalarKey() - exported.frame = keyframe.frame_num - exported.frameTime = keyframe.frame_time - exported.inTan = keyframe.in_tans[i] - exported.outTan = keyframe.out_tans[i] - exported.type = keyframe_type - exported.value = fval - exported_frames[i].append(exported) + keyframe_type = hsKeyFrame.kBezScalarKeyFrame if i in bez_chans else hsKeyFrame.kScalarKeyFrame + exported = hsScalarKey() + exported.frame = keyframe.frame_num + exported.frameTime = keyframe.frame_time + exported.inTan = keyframe.in_tans[i] + exported.outTan = keyframe.out_tans[i] + exported.type = keyframe_type + exported.value = keyframe.values[i] + exported_frames[i].append(exported) for i, subctrl in enumerate(subctrls): my_keyframes = exported_frames[i] - - # ensure this controller has at least ONE keyframe - if not my_keyframes: - hack_frame = hsScalarKey() - hack_frame.frame = 0 - hack_frame.frameTime = 0.0 - hack_frame.type = hsKeyFrame.kScalarKeyFrame - hack_frame.value = default_xform[i] - my_keyframes.append(hack_frame) getattr(ctrl, subctrl).keys = (my_keyframes, my_keyframes[0].type) return ctrl - def _make_scalar_leaf_controller(self, keyframes, bezier): + def _make_scalar_leaf_controller(self, keyframes, bezier) -> plLeafController: ctrl = plLeafController() keyframe_type = hsKeyFrame.kBezScalarKeyFrame if bezier else hsKeyFrame.kScalarKeyFrame exported_frames = [] @@ -600,239 +562,192 @@ class AnimationConverter: exported = hsScalarKey() exported.frame = keyframe.frame_num exported.frameTime = keyframe.frame_time - exported.inTan = keyframe.in_tan - exported.outTan = keyframe.out_tan + exported.inTan = keyframe.in_tans[0] + exported.outTan = keyframe.out_tans[0] exported.type = keyframe_type - exported.value = keyframe.value + exported.value = keyframe.values[0] exported_frames.append(exported) ctrl.keys = (exported_frames, keyframe_type) return ctrl - def _make_scale_value_controller(self, fcurves, keyframes, bez_chans, default_xform): - subctrls = ("X", "Y", "Z") + def _make_scale_value_controller(self, keyframes, bez_chans) -> plLeafController: keyframe_type = hsKeyFrame.kBezScaleKeyFrame if bez_chans else hsKeyFrame.kScaleKeyFrame exported_frames = [] - ctrl_fcurves = { i.array_index: i for i in fcurves } - default_scale = default_xform.to_scale() - unit_quat = default_xform.to_quaternion() - unit_quat.normalize() - unit_quat = utils.quaternion(unit_quat) + # Hmm... This smells... But it was basically doing this before the rewrite. + unit_quat = hsQuat(0.0, 0.0, 0.0, 1.0) for keyframe in keyframes: exported = hsScaleKey() exported.frame = keyframe.frame_num exported.frameTime = keyframe.frame_time exported.type = keyframe_type - - in_tan = hsVector3() - out_tan = hsVector3() - value = hsVector3() - for i, subctrl in enumerate(subctrls): - fval = keyframe.values.get(i, None) - if fval is not None: - setattr(value, subctrl, fval) - setattr(in_tan, subctrl, keyframe.in_tans[i]) - setattr(out_tan, subctrl, keyframe.out_tans[i]) - else: - try: - setattr(value, subctrl, ctrl_fcurves[i].evaluate(keyframe.frame_num_blender)) - except KeyError: - setattr(value, subctrl, default_scale[i]) - setattr(in_tan, subctrl, 0.0) - setattr(out_tan, subctrl, 0.0) - exported.inTan = in_tan - exported.outTan = out_tan - exported.value = (value, unit_quat) + exported.inTan = hsVector3(*keyframe.in_tans) + exported.outTan = hsVector3(*keyframe.out_tans) + exported.value = (hsVector3(*keyframe.values), unit_quat) exported_frames.append(exported) ctrl = plLeafController() ctrl.keys = (exported_frames, keyframe_type) return ctrl - def _process_fcurve(self, fcurve, convert=None): + def _sort_and_dedupe_keyframes(self, keyframes : Dict) -> Sequence: + """Takes in the final, unsorted keyframe sequence and sorts it. If all keyframes are + equivalent, eg due to a convert function, then they are discarded.""" + + num_keyframes = len(keyframes) + keyframes_sorted = [keyframes[i] for i in sorted(keyframes)] + + # If any keyframe's value is equivalent to its boundary keyframes, discard it. + def filter_boundaries(i): + if i == 0 or i == num_keyframes - 1: + return False + left, me, right = keyframes_sorted[i - 1], keyframes_sorted[i], keyframes_sorted[i + 1] + return left.values == me.values == right.values + + filtered_indices = list(itertools.filterfalse(filter_boundaries, range(num_keyframes))) + if len(filtered_indices) == 2: + if keyframes_sorted[filtered_indices[0]].values == keyframes_sorted[filtered_indices[1]].values: + return [] + return [keyframes_sorted[i] for i in filtered_indices] + + def _process_fcurve(self, fcurve, convert=None) -> Tuple[Sequence, AbstractSet]: """Like _process_keyframes, but for one fcurve""" + + # Adapt from incoming single item sequence to a single argument. + single_convert = lambda x: convert(x[0]) if convert is not None else None + # Can't proxy to _process_fcurves because it only supports linear interoplation. + return self._process_keyframes([fcurve], 1, [0.0], single_convert) + + def _santize_converted_values(self, num_channels : int, raw_values : Union[Dict, Sequence], convert : Callable): + assert convert is not None + if isinstance(raw_values, Dict): + values = convert(**raw_values) + elif isinstance(raw_values, Sequence): + values = convert(raw_values) + else: + raise AssertionError("Unexpected type for raw_values: {}".format(raw_values.__class__)) + + if not isinstance(values, Sequence) and isinstance(values, Iterable): + values = tuple(values) + if not isinstance(values, Sequence): + assert num_channels == 1, "Converter returned 1 value but expected {}".format(num_channels) + values = (values,) + else: + assert len(values) == num_channels, "Converter returned {} values but expected {}".format(len(values), num_channels) + return values + + def _process_fcurves(self, fcurves : Sequence, channels : Dict[str, int], result_channels : int, + convert : Callable, defaults : Dict[str, Union[float, Sequence]]) -> Sequence: + """This consumes a sequence of Blender FCurves that map to a single Plasma controller. + Like `_process_keyframes()`, except the converter function is mandatory, and each + Blender `data_path` must have a fixed number of channels. + """ + + # TODO: This fxn should probably issue a warning if any keyframes use bezier interpolation. + # But there's no indication given by any other fxn when an invalid interpolation mode is + # given, so what can you do? keyframe_data = type("KeyFrameData", (), {}) - fps = self._bl_fps - pi = math.pi + fps, pi = self._bl_fps, math.pi - keyframes = {} - bezier = False - fcurve.update() - for fkey in fcurve.keyframe_points: - keyframe = keyframe_data() - frame_num, value = fkey.co - if fps == 30.0: - keyframe.frame_num = int(frame_num) - else: - keyframe.frame_num = int(frame_num * (30.0 / fps)) - keyframe.frame_time = frame_num / fps - if fkey.interpolation == "BEZIER": - keyframe.in_tan = -(value - fkey.handle_left[1]) / (frame_num - fkey.handle_left[0]) / fps / (2 * pi) - keyframe.out_tan = (value - fkey.handle_right[1]) / (frame_num - fkey.handle_right[0]) / fps / (2 * pi) - bezier = True - else: - keyframe.in_tan = 0.0 - keyframe.out_tan = 0.0 - keyframe.value = value if convert is None else convert(value) - keyframes[frame_num] = keyframe - final_keyframes = [keyframes[i] for i in sorted(keyframes)] - return (final_keyframes, bezier) - - def _process_fcurves(self, fcurves, convert, defaults=None): - """Processes FCurves of different data sets and converts them into a single list of keyframes. - This should be used when multiple Blender fields map to a single Plasma option.""" - class KeyFrameData: - def __init__(self): - self.values = {} - fps = self._bl_fps - pi = math.pi - - # It is assumed therefore that any multichannel FCurves will have all channels represented. - # This seems fairly safe with my experiments with Lamp colors... - grouped_fcurves = {} - for fcurve in fcurves: - if fcurve is None: - continue + grouped_fcurves = defaultdict(dict) + for fcurve in (i for i in fcurves if i is not None): fcurve.update() - if fcurve.data_path in grouped_fcurves: - grouped_fcurves[fcurve.data_path][fcurve.array_index] = fcurve - else: - grouped_fcurves[fcurve.data_path] = { fcurve.array_index: fcurve } + grouped_fcurves[fcurve.data_path][fcurve.array_index] = fcurve - # Default values for channels that are not animated - for key, value in defaults.items(): - if key not in grouped_fcurves: - if hasattr(value, "__len__"): - grouped_fcurves[key] = value + fcurve_keyframes = defaultdict(functools.partial(defaultdict, dict)) + for fcurve in (i for i in fcurves if i is not None): + for fkey in fcurve.keyframe_points: + fcurve_keyframes[fkey.co[0]][fcurve.data_path][fcurve.array_index] = fkey + + def iter_channel_values(frame_num : int, fcurves : Dict, fkeys : Dict, num_channels : int, defaults : Union[float, Sequence]): + for i in range(num_channels): + fkey = fkeys.get(i, None) + if fkey is None: + fcurve = fcurves.get(i, None) + if fcurve is None: + # We would like to test this to see if it makes sense, but Blender's mathutils + # types don't actually implement the sequence protocol. So, we'll have to + # just try to subscript it and see what happens. + try: + yield defaults[i] + except: + assert num_channels == 1, "Got a non-subscriptable default for a multi-channel keyframe." + yield defaults + else: + yield fcurve.evaluate(frame_num) else: - grouped_fcurves[key] = [value,] + yield fkey.co[1] - # Assemble a dict { PlasmaFrameNum: { FCurveDataPath: KeyFrame } } - keyframe_points = {} - for fcurve in fcurves: - if fcurve is None: - continue - for keyframe in fcurve.keyframe_points: - frame_num_blender, value = keyframe.co - frame_num = int(frame_num_blender * (30.0 / fps)) - - # This is a temporary keyframe, so we're not going to worry about converting everything - # Only the frame number to Plasma so we can go ahead and merge any rounded dupes - entry, data = keyframe_points.get(frame_num), None - if entry is None: - entry = {} - keyframe_points[frame_num] = entry - else: - data = entry.get(fcurve.data_path) - if data is None: - data = KeyFrameData() - data.frame_num = frame_num - data.frame_num_blender = frame_num_blender - entry[fcurve.data_path] = data - data.values[fcurve.array_index] = value - - # Now, we loop through our assembled keyframes and interpolate any missing data using the FCurves - fcurve_chans = { key: len(value) for key, value in grouped_fcurves.items() } - expected_values = sum(fcurve_chans.values()) - all_chans = frozenset(grouped_fcurves.keys()) - - # We will also do the final convert here as well... - final_keyframes = [] - - for frame_num in sorted(keyframe_points.copy().keys()): - keyframes = keyframe_points[frame_num] - frame_num_blender = next(iter(keyframes.values())).frame_num_blender - - # If any data_paths are missing, init a dummy - missing_channels = all_chans - frozenset(keyframes.keys()) - for chan in missing_channels: - dummy = KeyFrameData() - dummy.frame_num = frame_num - dummy.frame_num_blender = frame_num_blender - keyframes[chan] = dummy - - # Ensure all values are filled out. - num_values = sum(map(len, (i.values for i in keyframes.values()))) - if num_values != expected_values: - for chan, sorted_fcurves in grouped_fcurves.items(): - chan_keyframes = keyframes[chan] - chan_values = fcurve_chans[chan] - if len(chan_keyframes.values) == chan_values: - continue - for i in range(chan_values): - if i not in chan_keyframes.values: - try: - fcurve = grouped_fcurves[chan][i] - except: - chan_keyframes.values[i] = defaults[chan] - else: - if isinstance(fcurve, bpy.types.FCurve): - chan_keyframes.values[i] = fcurve.evaluate(chan_keyframes.frame_num_blender) - else: - # it's actually a default value! - chan_keyframes.values[i] = fcurve - - # All values are calculated! Now we convert the disparate key data into a single keyframe. - kwargs = { data_path: keyframe.values for data_path, keyframe in keyframes.items() } - final_keyframe = KeyFrameData() - final_keyframe.frame_num = frame_num - final_keyframe.frame_num_blender = frame_num_blender - final_keyframe.frame_time = frame_num / fps - value = convert(**kwargs) - if hasattr(value, "__len__"): - final_keyframe.in_tans = [0.0] * len(value) - final_keyframe.out_tans = [0.0] * len(value) - final_keyframe.values = value - else: - final_keyframe.in_tan = 0.0 - final_keyframe.out_tan = 0.0 - final_keyframe.value = value - final_keyframes.append(final_keyframe) - return final_keyframes + keyframes = {} + for frame_num, fkeys in fcurve_keyframes.items(): + keyframe = keyframe_data() + # hope you don't have a frame 29.9 and frame 30.0... + keyframe.frame_num = int(frame_num * (30.0 / fps)) + keyframe.frame_num_blender = frame_num + keyframe.frame_time = frame_num / fps + keyframe.values_raw = { data_path: tuple(iter_channel_values(frame_num, grouped_fcurves[data_path], fkeys, num_channels, defaults[data_path])) + for data_path, num_channels in channels.items() } + keyframe.values = self._santize_converted_values(result_channels, keyframe.values_raw, convert) + # Very gnawty + keyframe.in_tans = [0.0] * result_channels + keyframe.out_tans = [0.0] * result_channels + keyframes[frame_num] = keyframe - 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""" keyframe_data = type("KeyFrameData", (), {}) - fps = self._bl_fps - pi = math.pi + fps, pi = self._bl_fps, math.pi - keyframes = {} - bez_chans = set() - for fcurve in fcurves: + keyframes, fcurve_keyframes = {}, defaultdict(dict) + + indexed_fcurves = { fcurve.array_index: fcurve for fcurve in fcurves if fcurve is not None } + for i, fcurve in indexed_fcurves.items(): fcurve.update() for fkey in fcurve.keyframe_points: - frame_num, value = fkey.co - keyframe = keyframes.get(frame_num, None) - if keyframe is None: - keyframe = keyframe_data() - if fps == 30.0: - # hope you don't have a frame 29.9 and frame 30.0... - keyframe.frame_num = int(frame_num) - else: - keyframe.frame_num = int(frame_num * (30.0 / fps)) - keyframe.frame_num_blender = frame_num - keyframe.frame_time = frame_num / fps - keyframe.in_tans = {} - keyframe.out_tans = {} - keyframe.values = {} - keyframes[frame_num] = keyframe - idx = fcurve.array_index - keyframe.values[idx] = value if convert is None else convert(value) - - # Calculate the bezier interpolation nonsense - if fkey.interpolation == "BEZIER": - keyframe.in_tans[idx] = -(value - fkey.handle_left[1]) / (frame_num - fkey.handle_left[0]) / fps / (2 * pi) - keyframe.out_tans[idx] = (value - fkey.handle_right[1]) / (frame_num - fkey.handle_right[0]) / fps / (2 * pi) - bez_chans.add(idx) + fcurve_keyframes[fkey.co[0]][i] = fkey + + def iter_values(frame_num, fkeys) -> Generator[float, None, None]: + for i in range(num_channels): + fkey = fkeys.get(i, None) + if fkey is not None: + yield fkey.co[1] else: - keyframe.in_tans[idx] = 0.0 - keyframe.out_tans[idx] = 0.0 + fcurve = indexed_fcurves.get(i, None) + if fcurve is not None: + yield fcurve.evaluate(frame_num) + else: + yield default_values[i] + + # Does this really need to be a set? + bez_chans = set() + + for frame_num, fkeys in fcurve_keyframes.items(): + keyframe = keyframe_data() + # hope you don't have a frame 29.9 and frame 30.0... + keyframe.frame_num = int(frame_num * (30.0 / fps)) + keyframe.frame_num_blender = frame_num + keyframe.frame_time = frame_num / fps + keyframe.in_tans = [0.0] * num_channels + keyframe.out_tans = [0.0] * num_channels + keyframe.values_raw = tuple(iter_values(frame_num, fkeys)) + if convert is None: + keyframe.values = keyframe.values_raw + else: + keyframe.values = self._santize_converted_values(num_channels, keyframe.values_raw, convert) + + for i, fkey in ((i, fkey) for i, fkey in fkeys.items() if fkey.interpolation == "BEZIER"): + value = keyframe.values_raw[i] + keyframe.in_tans[i] = -(value - fkey.handle_left[1]) / (frame_num - fkey.handle_left[0]) / fps / (2 * pi) + keyframe.out_tans[i] = (value - fkey.handle_right[1]) / (frame_num - fkey.handle_right[0]) / fps / (2 * pi) + bez_chans.add(i) + keyframes[frame_num] = keyframe # Return the keyframes in a sequence sorted by frame number - final_keyframes = [keyframes[i] for i in sorted(keyframes)] - return (final_keyframes, bez_chans) + return (self._sort_and_dedupe_keyframes(keyframes), bez_chans) @property def _mgr(self): From 763b086b9c89af15fb8f16c3712dfb95936fab45 Mon Sep 17 00:00:00 2001 From: Adam Johnson Date: Thu, 18 Feb 2021 19:17:05 -0500 Subject: [PATCH 47/50] Implement material diffuse animations. Closes #188. --- korman/exporter/material.py | 92 +++++++++++++++++++++++++------------ 1 file changed, 63 insertions(+), 29 deletions(-) diff --git a/korman/exporter/material.py b/korman/exporter/material.py index 5cc3fa3..06d1f21 100644 --- a/korman/exporter/material.py +++ b/korman/exporter/material.py @@ -16,8 +16,10 @@ import bpy import functools import math +import mathutils from pathlib import Path from PyHSPlasma import * +from typing import Union import weakref from .explosions import * @@ -143,7 +145,10 @@ class MaterialConverter: "NONE": self._export_texture_type_none, } self._animation_exporters = { + "ambientCtl": functools.partial(self._export_layer_diffuse_animation, converter=self.get_material_ambient), "opacityCtl": self._export_layer_opacity_animation, + "preshadeCtl": functools.partial(self._export_layer_diffuse_animation, converter=self.get_material_preshade), + "runtimeCtl": functools.partial(self._export_layer_diffuse_animation, converter=self.get_material_runtime), "transformCtl": self._export_layer_transform_animation, } @@ -254,6 +259,7 @@ class MaterialConverter: if not hsgmat.layers: layer = self._mgr.find_create_object(plLayer, name="{}_AutoLayer".format(mat_name), bl=bo) self._propagate_material_settings(bo, bm, layer) + layer = self._export_layer_animations(bo, bm, None, 0, layer) hsgmat.addLayer(layer.key) # Cache this material for later @@ -489,7 +495,7 @@ class MaterialConverter: layer = self._export_layer_animations(bo, bm, slot, idx, layer) return layer - def _export_layer_animations(self, bo, bm, tex_slot, idx, base_layer): + def _export_layer_animations(self, bo, bm, tex_slot, idx, base_layer) -> plLayer: """Exports animations on this texture and chains the Plasma layers as needed""" def harvest_fcurves(bl_id, collection, data_path=None): @@ -508,9 +514,16 @@ class MaterialConverter: return None fcurves = [] - texture = tex_slot.texture - mat_action = harvest_fcurves(bm, fcurves, "texture_slots[{}]".format(idx)) - tex_action = harvest_fcurves(texture, fcurves) + + # Base layers get all of the fcurves for animating things like the diffuse color + texture = tex_slot.texture if tex_slot is not None else None + if idx == 0: + harvest_fcurves(bm, fcurves) + harvest_fcurves(texture, fcurves) + elif tex_slot is not None: + harvest_fcurves(bm, fcurves, tex_slot.path_from_id()) + harvest_fcurves(texture, fcurves) + if not fcurves: return base_layer @@ -518,7 +531,7 @@ class MaterialConverter: # and chain this biotch up as best we can. layer_animation = None for attr, converter in self._animation_exporters.items(): - ctrl = converter(tex_slot, base_layer, fcurves) + ctrl = converter(bo, bm, tex_slot, base_layer, fcurves) if ctrl is not None: if layer_animation is None: name = "{}_LayerAnim".format(base_layer.key.name) @@ -539,21 +552,39 @@ class MaterialConverter: 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 - layer_props = tex_slot.texture.plasma_layer - if not layer_props.anim_auto_start: - atc.flags |= plAnimTimeConvert.kStopped - if layer_props.anim_loop: + if tex_slot is not None: + layer_props = tex_slot.texture.plasma_layer + if not layer_props.anim_auto_start: + atc.flags |= plAnimTimeConvert.kStopped + if layer_props.anim_loop: + atc.flags |= plAnimTimeConvert.kLoop + atc.loopBegin = atc.begin + atc.loopEnd = atc.end + if layer_props.anim_sdl_var: + layer_animation.varName = layer_props.anim_sdl_var + else: + # Hmm... I wonder what we should do here? A reasonable default might be to just + # run the stupid thing in a loop. atc.flags |= plAnimTimeConvert.kLoop atc.loopBegin = atc.begin atc.loopEnd = atc.end - if layer_props.anim_sdl_var: - layer_animation.varName = layer_props.anim_sdl_var return layer_animation # Well, we had some FCurves but they were garbage... Too bad. return base_layer - def _export_layer_opacity_animation(self, tex_slot, base_layer, fcurves): + def _export_layer_diffuse_animation(self, bo, bm, tex_slot, base_layer, fcurves, converter): + assert converter is not None + + def translate_color(color_sequence): + # See things like get_material_preshade + result = converter(bo, bm, mathutils.Color(color_sequence)) + return result.red, result.green, result.blue + + ctrl = self._exporter().animation.make_pos_controller(fcurves, "diffuse_color", bm.diffuse_color, translate_color) + return ctrl + + def _export_layer_opacity_animation(self, bo, bm, tex_slot, base_layer, fcurves): for i in fcurves: if i.data_path == "plasma_layer.opacity": base_layer.state.blendFlags |= hsGMatState.kBlendAlpha @@ -561,14 +592,16 @@ class MaterialConverter: return ctrl return None - def _export_layer_transform_animation(self, tex_slot, base_layer, fcurves): - path = tex_slot.path_from_id() - pos_path = "{}.offset".format(path) - scale_path = "{}.scale".format(path) + def _export_layer_transform_animation(self, bo, bm, tex_slot, base_layer, fcurves): + if tex_slot is not None: + path = tex_slot.path_from_id() + pos_path = "{}.offset".format(path) + scale_path = "{}.scale".format(path) - # Plasma uses the controller to generate a matrix44... so we have to produce a leaf controller - ctrl = self._exporter().animation.make_matrix44_controller(fcurves, pos_path, scale_path, tex_slot.offset, tex_slot.scale) - return ctrl + # Plasma uses the controller to generate a matrix44... so we have to produce a leaf controller + ctrl = self._exporter().animation.make_matrix44_controller(fcurves, pos_path, scale_path, tex_slot.offset, tex_slot.scale) + return ctrl + return None def _export_texture_type_environment_map(self, bo, layer, slot): """Exports a Blender EnvironmentMapTexture to a plLayer""" @@ -1164,24 +1197,26 @@ class MaterialConverter: def get_bump_layer(self, bo): return self._bump_mats.get(bo, None) - def get_material_ambient(self, bo, bm) -> hsColorRGBA: + 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: - return hsColorRGBA(bm.diffuse_color.r * emit_scale, - bm.diffuse_color.g * emit_scale, - bm.diffuse_color.b * emit_scale, + 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=None) -> hsColorRGBA: + 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=None) -> hsColorRGBA: + 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: @@ -1191,19 +1226,18 @@ class MaterialConverter: def get_texture_animation_key(self, bo, bm, texture): """Finds or creates the appropriate key for sending messages to an animated Texture""" - tex_name = texture.name + tex_name = texture.name if texture is not None else "AutoLayer" if bo.type == "LAMP": assert bm is None bm_name = bo.name else: assert bm is not None bm_name = bm.name - if not tex_name in bm.texture_slots: + if texture is not None and not tex_name in bm.texture_slots: raise ExportError("Texture '{}' not used in Material '{}'".format(bm_name, tex_name)) name = "{}_{}_LayerAnim".format(bm_name, tex_name) - layer = texture.plasma_layer - pClass = plLayerSDLAnimation if layer.anim_sdl_var else plLayerAnimation + pClass = plLayerSDLAnimation if texture is not None and texture.plasma_layer.anim_sdl_var else plLayerAnimation return self._mgr.find_create_key(pClass, bl=bo, name=name) @property From a893ac2725f3203e1bbe2c1da132ef4a7600a5df Mon Sep 17 00:00:00 2001 From: Adam Johnson Date: Mon, 22 Feb 2021 15:22:43 -0500 Subject: [PATCH 48/50] Fix syntax goof that eats opacity animations. --- korman/exporter/animation.py | 6 +++++- korman/exporter/material.py | 1 - 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/korman/exporter/animation.py b/korman/exporter/animation.py index f800f55..58b3c20 100644 --- a/korman/exporter/animation.py +++ b/korman/exporter/animation.py @@ -614,8 +614,12 @@ class AnimationConverter: def _process_fcurve(self, fcurve, convert=None) -> Tuple[Sequence, AbstractSet]: """Like _process_keyframes, but for one fcurve""" + self._exporter().report.msg("_process_fcurve") # Adapt from incoming single item sequence to a single argument. - single_convert = lambda x: convert(x[0]) if convert is not None else None + if convert is not None: + single_convert = lambda x: convert(x[0]) + else: + single_convert = None # Can't proxy to _process_fcurves because it only supports linear interoplation. return self._process_keyframes([fcurve], 1, [0.0], single_convert) diff --git a/korman/exporter/material.py b/korman/exporter/material.py index 06d1f21..95bb2a5 100644 --- a/korman/exporter/material.py +++ b/korman/exporter/material.py @@ -534,7 +534,6 @@ class MaterialConverter: ctrl = converter(bo, bm, tex_slot, base_layer, fcurves) if ctrl is not None: if layer_animation is None: - name = "{}_LayerAnim".format(base_layer.key.name) layer_animation = self.get_texture_animation_key(bo, bm, texture).object setattr(layer_animation, attr, ctrl) From d6072b6689bb2ac02a405575a4b54036be94bf92 Mon Sep 17 00:00:00 2001 From: Adam Johnson Date: Mon, 22 Feb 2021 15:31:03 -0500 Subject: [PATCH 49/50] Fix crash in Path of the Shell related to empty animations. --- korman/properties/modifiers/anim.py | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/korman/properties/modifiers/anim.py b/korman/properties/modifiers/anim.py index 3f6c4f0..c836df4 100644 --- a/korman/properties/modifiers/anim.py +++ b/korman/properties/modifiers/anim.py @@ -66,12 +66,19 @@ class PlasmaAnimationModifier(ActionModifier, PlasmaModifierProperties): @property def anim_type(self): - return plAgeGlobalAnim if self.obj_sdl_anim else plATCAnim + return plAgeGlobalAnim if self.enabled and self.obj_sdl_anim else plATCAnim def export(self, exporter, bo, so): action = self.blender_action anim_mod = bo.plasma_modifiers.animation - atcanim = exporter.mgr.find_create_object(anim_mod.anim_type, 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.loop = self.loop From 30ee43b2b7ea1ed625117055f249f3a5db23e582 Mon Sep 17 00:00:00 2001 From: Adam Johnson Date: Mon, 22 Feb 2021 15:32:24 -0500 Subject: [PATCH 50/50] Nuke accidental debug print. --- korman/exporter/animation.py | 1 - 1 file changed, 1 deletion(-) diff --git a/korman/exporter/animation.py b/korman/exporter/animation.py index 58b3c20..e37b653 100644 --- a/korman/exporter/animation.py +++ b/korman/exporter/animation.py @@ -614,7 +614,6 @@ class AnimationConverter: def _process_fcurve(self, fcurve, convert=None) -> Tuple[Sequence, AbstractSet]: """Like _process_keyframes, but for one fcurve""" - self._exporter().report.msg("_process_fcurve") # Adapt from incoming single item sequence to a single argument. if convert is not None: single_convert = lambda x: convert(x[0])