From 6ed81d2794dc953e99889124c285fee738ad1771 Mon Sep 17 00:00:00 2001 From: Adam Johnson Date: Thu, 30 Jun 2016 20:32:26 -0400 Subject: [PATCH] Improve stenciling support. NOTE: BREAKING CHANGE OK, this commit does a lot, and it was hard to separate the changes, so mega-commit. - We now export stencils in such a way that Plasma layers can be affected by multiple stencils, theoretically matching the Blender behavior - We don't try to animate stencils or even offer animation options. This is nonsense. - In our zealousness to skip over illegal texture slots, there was a bug introduced in which we queried texture fcurve paths illegally, potentially causing animations to be exported incorrectly. This has been fixed. - There was a potential issue with the animation command node's material option in which the message could be sent to the wrong page. This has been corrected by making the Object field mandatory for all animation types. --- korman/exporter/material.py | 95 +++++++++++++++++++---------------- korman/exporter/rtlight.py | 2 +- korman/nodes/node_messages.py | 32 ++++++------ korman/ui/ui_texture.py | 5 +- 4 files changed, 74 insertions(+), 60 deletions(-) diff --git a/korman/exporter/material.py b/korman/exporter/material.py index 1e3509c..6247cbd 100644 --- a/korman/exporter/material.py +++ b/korman/exporter/material.py @@ -24,6 +24,8 @@ from .. import helpers from ..korlib import * from . import utils +_MAX_STENCILS = 6 + class _Texture: _DETAIL_BLEND = { TEX_DETAIL_ALPHA: "AL", @@ -125,14 +127,40 @@ class MaterialConverter: print(" Exporting Material '{}'".format(bm.name)) hsgmat = self._mgr.add_object(hsGMaterial, name=bm.name, bl=bo) - slots = [slot for slot in bm.texture_slots if slot is not None and slot.use and - slot.texture is not None and slot.texture.type in self._tex_exporters] - - # Okay, I know this isn't Pythonic... But we're doing it this way because we might actually - # export many slots in one go. Think stencils. - i = 0 - while i < len(slots): - i += self.export_texture_slot(bo, bm, hsgmat, slots, i) + slots = [(idx, slot) for idx, slot in enumerate(bm.texture_slots) if slot is not None and slot.use \ + and slot.texture is not None and slot.texture.type in self._tex_exporters] + + # There is a major difference in how Blender and Plasma handle stencils. + # In Blender, the stencil is on top and applies to every layer below is. In Plasma, the stencil + # is below the SINGLE layer it affects. The main texture is marked BindNext and RestartPassHere. + # The pipeline indicates that we can render 8 layers simultaneously, so we will collect all + # stencils and apply this arrangement. We're going to limit to 6 stencils however. 1 layer for + # main texture and 1 piggyback. + num_stencils = sum((1 for i in slots if i[1].use_stencil)) + if num_stencils > _MAX_STENCILS: + raise ExportError("Material '{}' uses too many stencils. The maximum is {}".format(bm.name, _MAX_STENCILS)) + stencils = [] + + # Loop over layers + for idx, slot in slots: + if slot.use_stencil: + stencils.append((idx, slot)) + else: + tex_layer = self.export_texture_slot(bo, bm, hsgmat, slot, idx) + hsgmat.addLayer(tex_layer.key) + if stencils: + tex_state = tex_layer.state + if not tex_state.blendFlags & hsGMatState.kBlendMask: + tex_state.blendFlags |= hsGMatState.kBlendAlpha + tex_state.miscFlags |= hsGMatState.kMiscRestartPassHere | hsGMatState.kMiscBindNext + curr_stencils = len(stencils) + for i in range(curr_stencils): + stencil_idx, stencil = stencils[i] + stencil_name = "STENCILGEN_{}@{}_{}".format(stencil.name, bm.name, slot.name) + stencil_layer = self.export_texture_slot(bo, bm, hsgmat, stencil, stencil_idx, name=stencil_name) + if i+1 < curr_stencils: + stencil_layer.state.miscFlags |= hsGMatState.kMiscBindNext + hsgmat.addLayer(stencil_layer.key) # 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 @@ -166,11 +194,9 @@ class MaterialConverter: # Wasn't that easy? return hsgmat.key - def export_texture_slot(self, bo, bm, hsgmat, slots, idx, blend_flags=True): - slot = slots[idx] - num_exported = 1 - - name = "{}_{}".format(bm.name if bm is not None else bo.name, slot.name) + def export_texture_slot(self, bo, bm, hsgmat, slot, idx, name=None, blend_flags=True): + if name is None: + name = "{}_{}".format(bm.name if bm is not None else bo.name, slot.name) print(" Exporting Plasma Layer '{}'".format(name)) layer = self._mgr.add_object(plLayer, name=name, bl=bo) if bm is not None: @@ -204,22 +230,6 @@ class MaterialConverter: state.clampFlags |= hsGMatState.kClampTexture state.ZFlags |= hsGMatState.kZNoZWrite layer.ambient = hsColorRGBA(1.0, 1.0, 1.0, 1.0) - - # Plasma actually wants the next layer first, so let's export him - nextIdx = idx + 1 - if len(slots) == nextIdx: - raise ExportError("Texture Slot '{}' wants to be a stencil, but there are no more TextureSlots.".format(slot.name)) - print(" --- BEGIN STENCIL ---") - self.export_texture_slot(bo, bm, hsgmat, slots, nextIdx) - print(" --- END STENCIL ---") - num_exported += 1 - - # Now that we've exported the bugger, flag him as binding with this texture - prev_layer = hsgmat.layers[-1].object - prev_state = prev_layer.state - prev_state.miscFlags |= hsGMatState.kMiscBindNext | hsGMatState.kMiscRestartPassHere - if not prev_state.blendFlags & hsGMatState.kBlendMask: - prev_state.blendFlags |= hsGMatState.kBlendAlpha elif blend_flags: # Standard layer flags ahoy if slot.blend_type == "ADD": @@ -241,13 +251,10 @@ class MaterialConverter: self._tex_exporters[texture.type](bo, layer, slot) # Export any layer animations - layer = self._export_layer_animations(bo, bm, slot, idx, layer) - - if hsgmat is None: - return layer - else: - hsgmat.addLayer(layer.key) - return num_exported + # NOTE: animated stencils are nonsense. + if slot.use_stencil: + layer = self._export_layer_animations(bo, bm, slot, idx, layer) + return layer def _export_layer_animations(self, bo, bm, tex_slot, idx, base_layer): """Exports animations on this texture and chains the Plasma layers as needed""" @@ -263,17 +270,13 @@ class MaterialConverter: if data_path is None: collection.extend(action.fcurves) else: - collection.extend([i for i in action.fcurves if i.data_path.startswith(data_path)]) + collection.extend((i for i in action.fcurves if i.data_path.startswith(data_path))) return action return None - # First, we must gather relevant FCurves from both the material and the texture itself - # Because, you know, that totally makes sense... fcurves = [] mat_action = harvest_fcurves(bm, fcurves, "texture_slots[{}]".format(idx)) - tex_action = harvest_fcurves(tex_slot.texture, fcurves) - - # No fcurves, no animation + tex_action = harvest_fcurves(bm.texture_slots[idx].texture, fcurves) if not fcurves: return base_layer @@ -589,6 +592,14 @@ class MaterialConverter: def get_materials(self, bo): return self._obj2mat[bo] + def get_texture_animation_key(self, bo, bm, tex_name): + """Finds or creates the appropriate key for sending messages to an animated Texture""" + tex_slot = bm.texture_slots.get(tex_name, None) + if tex_slot is None: + raise ExportError("Material '{}' does not contain Texture '{}'".format(bm.name, tex_name)) + name = "{}_{}_LayerAnim".format(bm.name, tex_name) + return self._mgr.find_create_key(plLayerAnimation, bl=bo, name=name) + @property def _mgr(self): return self._exporter().mgr diff --git a/korman/exporter/rtlight.py b/korman/exporter/rtlight.py index 66df032..b343f81 100644 --- a/korman/exporter/rtlight.py +++ b/korman/exporter/rtlight.py @@ -203,7 +203,7 @@ class LightConverter: # one layer. We could exploit the fUnderLay and fOverLay system to export everything, but meh. if len(tex_slots) > 1: self._exporter().warn("Only one texture slot can be exported per Lamp. Picking the first one: '{}'".format(slot.name), indent=3) - layer = mat.export_texture_slot(bo, None, None, tex_slots, 0, blend_flags=False) + layer = mat.export_texture_slot(bo, None, None, slot, 0, blend_flags=False) state = layer.state # Colors science'd from PRPs diff --git a/korman/nodes/node_messages.py b/korman/nodes/node_messages.py index 05e28d8..23abdf0 100644 --- a/korman/nodes/node_messages.py +++ b/korman/nodes/node_messages.py @@ -121,13 +121,19 @@ class PlasmaAnimCmdMsgNode(PlasmaMessageNode, bpy.types.Node): def draw_buttons(self, context, layout): layout.prop(self, "anim_type") - if self.anim_type == "OBJECT": - layout.prop_search(self, "object_name", bpy.data, "objects") - else: - layout.prop_search(self, "material_name", bpy.data, "materials") - material = bpy.data.materials.get(self.material_name, None) - if material is not None: - layout.prop_search(self, "texture_name", material, "texture_slots") + layout.prop_search(self, "object_name", bpy.data, "objects") + + if self.anim_type != "OBJECT": + bo = bpy.data.objects.get(self.object_name) + if bo is None or not hasattr(bo.data, "materials"): + layout.label("Invalid Object", icon="ERROR") + else: + layout.prop_search(self, "material_name", bo.data, "materials") + material = bpy.data.materials.get(self.material_name, None) + if material is None: + layout.label("Invalid Material", icon="ERROR") + else: + layout.prop_search(self, "texture_name", material, "texture_slots") layout.prop(self, "go_to") layout.prop(self, "action") @@ -164,10 +170,10 @@ class PlasmaAnimCmdMsgNode(PlasmaMessageNode, bpy.types.Node): msg = plAnimCmdMsg() # We're either sending this off to an AGMasterMod or a LayerAnim + obj = bpy.data.objects.get(self.object_name, None) + if obj is None: + self.raise_error("invalid object: '{}'".format(self.object_name)) if self.anim_type == "OBJECT": - obj = bpy.data.objects.get(self.object_name, None) - if obj is None: - self.raise_error("invalid object: '{}'".format(self.object_name)) if not exporter.animation.is_animated(obj): self.raise_error("invalid animation") group = obj.plasma_modifiers.animation_group @@ -182,11 +188,7 @@ class PlasmaAnimCmdMsgNode(PlasmaMessageNode, bpy.types.Node): material = bpy.data.materials.get(self.material_name, None) if material is None: self.raise_error("invalid material: '{}'".format(self.material_name)) - tex_slot = material.texture_slots.get(self.texture_name, None) - if tex_slot is None: - self.raise_error("invalid texture: '{}'".format(self.texture_name)) - name = "{}_{}_LayerAnim".format(self.material_name, self.texture_name) - target = exporter.mgr.find_create_key(plLayerAnimation, name=name, so=so) + target = exporter.mesh.material.get_texture_animation_key(obj, material, self.texture_name) if target is None: raise RuntimeError() msg.addReceiver(target) diff --git a/korman/ui/ui_texture.py b/korman/ui/ui_texture.py index afaa99b..b14424c 100644 --- a/korman/ui/ui_texture.py +++ b/korman/ui/ui_texture.py @@ -62,7 +62,7 @@ class PlasmaLayerPanel(TextureButtonsPanel, bpy.types.Panel): bl_label = "Plasma Layer Options" def draw(self, context): - texture = context.texture + texture, slot = context.texture, context.texture_slot layer_props = texture.plasma_layer layout = self.layout @@ -84,12 +84,13 @@ class PlasmaLayerPanel(TextureButtonsPanel, bpy.types.Panel): split = layout.split() col = split.column() col.label("Animation:") - col.enabled = self._has_animation_data(context) + col.enabled = self._has_animation_data(context) and not slot.use_stencil col.prop(layer_props, "anim_auto_start") col.prop(layer_props, "anim_loop") col = split.column() col.label("General:") + col.active = not slot.use_stencil col.prop(layer_props, "opacity", text="Opacity") col.prop(layer_props, "alpha_halo")