From 7d7af297e466c9211eb927660cffea388b9de12f Mon Sep 17 00:00:00 2001 From: Adam Johnson Date: Mon, 23 Nov 2015 23:33:31 -0500 Subject: [PATCH] SoftVolume Modifier This introduces a basic soft volume interface to Korman. It currently allows you to define simple convex "soft" regions and somewhat complex soft regions using the soft volume modifier. --- korman/nodes/__init__.py | 2 + korman/nodes/node_core.py | 26 ++++ korman/nodes/node_softvolume.py | 195 ++++++++++++++++++++++++++ korman/properties/modifiers/region.py | 91 ++++++++++++ korman/ui/modifiers/region.py | 18 +++ 5 files changed, 332 insertions(+) create mode 100644 korman/nodes/node_softvolume.py diff --git a/korman/nodes/__init__.py b/korman/nodes/__init__.py index b3dcb06..f1670ef 100644 --- a/korman/nodes/__init__.py +++ b/korman/nodes/__init__.py @@ -26,6 +26,7 @@ from .node_logic import * from .node_messages import * from .node_python import * from .node_responder import * +from .node_softvolume import * class PlasmaNodeCategory(NodeCategory): """Plasma Node Category""" @@ -45,6 +46,7 @@ _kategory_names = { "LOGIC": "Logic", "MSG": "Message", "PYTHON": "Python", + "SV": "Soft Volume", } # Now, generate the categories as best we can... diff --git a/korman/nodes/node_core.py b/korman/nodes/node_core.py index 86fbb0e..4a0e310 100644 --- a/korman/nodes/node_core.py +++ b/korman/nodes/node_core.py @@ -52,6 +52,14 @@ class PlasmaNodeBase: return None raise KeyError(key) + def find_inputs(self, key, idname=None): + for i in self.inputs: + if i.alias == key: + if i.links: + node = i.links[0].from_node + if idname is None or idname == node.bl_idname: + yield node + def find_input_socket(self, key): for i in self.inputs: if i.alias == key: @@ -223,6 +231,18 @@ class PlasmaNodeBase: socket.link_limit = link_limit +class PlasmaTreeOutputNodeBase(PlasmaNodeBase): + """Represents the final output of a node tree""" + def init(self, context): + nodes = self.id_data.nodes + + # There can only be one of these nodes per tree, so let's make sure I'm the only one. + for i in nodes: + if isinstance(i, self.__class__) and i != self: + nodes.remove(self) + return + + class PlasmaNodeSocketBase: @property def alias(self): @@ -265,6 +285,12 @@ class PlasmaNodeTree(bpy.types.NodeTree): for node in self.nodes: node.export(exporter, bo, so) + def find_output(self, idname): + for node in self.nodes: + if node.bl_idname == idname: + return node + return None + def harvest_actors(self): actors = set() for node in self.nodes: diff --git a/korman/nodes/node_softvolume.py b/korman/nodes/node_softvolume.py new file mode 100644 index 0000000..5eae4c1 --- /dev/null +++ b/korman/nodes/node_softvolume.py @@ -0,0 +1,195 @@ +# This file is part of Korman. +# +# Korman is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Korman is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Korman. If not, see . + +import bpy +from bpy.props import * +from collections import OrderedDict +from PyHSPlasma import * + +from .node_core import PlasmaNodeBase, PlasmaNodeSocketBase, PlasmaTreeOutputNodeBase + +class PlasmaSoftVolumeOutputNode(PlasmaTreeOutputNodeBase, bpy.types.Node): + bl_category = "SV" + bl_idname = "PlasmaSoftVolumeOutputNode" + bl_label = "Soft Volume Output" + + input_sockets = OrderedDict([ + ("input", { + "text": "Final Volume", + "type": "PlasmaSoftVolumeNodeSocket", + }), + ]) + + def get_key(self, exporter, so): + svNode = self.find_input("input") + if svNode is not None: + return svNode.get_key(exporter, so) + return None + + +class PlasmaSoftVolumeNodeSocket(PlasmaNodeSocketBase, bpy.types.NodeSocket): + bl_color = (0.133, 0.094, 0.345, 1.0) +class PlasmaSoftVolumePropertiesNodeSocket(PlasmaNodeSocketBase, bpy.types.NodeSocket): + bl_color = (0.067, 0.40, 0.067, 1.0) + + +class PlasmaSoftVolumePropertiesNode(PlasmaNodeBase, bpy.types.Node): + bl_category = "SV" + bl_idname = "PlasmaSoftVolumePropertiesNode" + bl_label = "Soft Volume Properties" + + output_sockets = OrderedDict([ + ("target", { + "text": "Volume", + "type": "PlasmaSoftVolumePropertiesNodeSocket" + }), + ]) + + inside_strength = IntProperty(name="Inside", description="Strength inside the region", + subtype="PERCENTAGE", default=100, min=0, max=100) + outside_strength = IntProperty(name="Outside", description="Strength outside the region", + subtype="PERCENTAGE", default=0, min=0, max=100) + + def draw_buttons(self, context, layout): + layout.prop(self, "inside_strength") + layout.prop(self, "outside_strength") + + def propagate(self, softvolume): + softvolume.insideStrength = self.inside_strength / 100 + softvolume.outsideStrength = self.outside_strength / 100 + + +class PlasmaSoftVolumeReferenceNode(PlasmaNodeBase, bpy.types.Node): + bl_category = "SV" + bl_idname = "PlasmaSoftVolumeReferenceNode" + bl_label = "Soft Region" + bl_width_default = 150 + + output_sockets = OrderedDict([ + ("output", { + "text": "Volume", + "type": "PlasmaSoftVolumeNodeSocket" + }), + ]) + + soft_object = StringProperty(name="Soft Volume", + description="Object whose Soft Volume modifier we should use") + + def draw_buttons(self, context, layout): + layout.prop_search(self, "soft_object", bpy.data, "objects", icon="OBJECT_DATA", text="") + + def get_key(self, exporter, so): + softvol = bpy.data.objects.get(self.soft_object, None) + if softvol is None: + self.raise_error("Volume Object '{}' not found".format(self.soft_object)) + # Don't use SO here because that's the tree owner's SO. This soft region will find or create + # its own SceneObject. Yay! + return softvol.plasma_modifiers.softvolume.get_key(exporter) + + +class PlasmaSoftVolumeInvertNode(PlasmaNodeBase, bpy.types.Node): + bl_category = "SV" + bl_idname = "PlasmaSoftVolumeInvertNode" + bl_label = "Soft Volume Invert" + + # The only difference between this and PlasmaSoftVolumeLinkNode is this can only have ONE input + input_sockets = OrderedDict([ + ("properties", { + "text": "Properties", + "type": "PlasmaSoftVolumePropertiesNodeSocket", + }), + ("input", { + "text": "Input Volume", + "type": "PlasmaSoftVolumeNodeSocket", + }), + ]) + + output_sockets = OrderedDict([ + ("output", { + "text": "Output Volume", + "type": "PlasmaSoftVolumeNodeSocket" + }), + ]) + + def get_key(self, exporter, so): + return exporter.mgr.find_create_key(plSoftVolumeInvert, name=self.key_name, so=so) + + def export(self, exporter, bo, so): + parent = self.find_input("input") + if parent is None: + self.raise_error("SoftVolume Invert requires an input volume!") + + sv = self.get_key(exporter, so).object + sv.addSubVolume(parent.get_key(exporter, so)) + + props = self.find_input("properties") + if props is not None: + props.propagate(sv) + else: + sv.insideStrength = 1.0 + sv.outsideStrength = 0.0 + + +class PlasmaSoftVolumeLinkNode(PlasmaNodeBase): + input_sockets = OrderedDict([ + ("properties", { + "text": "Properties", + "type": "PlasmaSoftVolumePropertiesNodeSocket", + }), + ("input", { + "text": "Input Volume", + "type": "PlasmaSoftVolumeNodeSocket", + "spawn_empty": True, + }), + ]) + + output_sockets = OrderedDict([ + ("output", { + "text": "Output Volume", + "type": "PlasmaSoftVolumeNodeSocket" + }), + ]) + + def export(self, exporter, bo, so): + sv = self.get_key(exporter, so).object + for node in self.find_inputs("input"): + sv.addSubVolume(node.get_key(exporter, so)) + + props = self.find_input("properties") + if props is not None: + props.propagate(sv) + else: + sv.insideStrength = 1.0 + sv.outsideStrength = 0.0 + + +class PlasmaSoftVolumeIntersectNode(PlasmaSoftVolumeLinkNode, bpy.types.Node): + bl_category = "SV" + bl_idname = "PlasmaSoftVolumeIntersectNode" + bl_label = "Soft Volume Intersect" + + def get_key(self, exporter, so): + ## FIXME: SoftVolumeIntersect should not be listed as an interface + return exporter.mgr.find_create_key(plSoftVolumeIntersect, name=self.key_name, so=so) + + +class PlasmaSoftVolumeUnionNode(PlasmaSoftVolumeLinkNode, bpy.types.Node): + bl_category = "SV" + bl_idname = "PlasmaSoftVolumeUnionNode" + bl_label = "Soft Volume Union" + + def get_key(self, exporter, so): + ## FIXME: SoftVolumeUnion should not be listed as an interface + return exporter.mgr.find_create_key(plSoftVolumeUnion, name=self.key_name, so=so) diff --git a/korman/properties/modifiers/region.py b/korman/properties/modifiers/region.py index 4dd8b2f..8101cd9 100644 --- a/korman/properties/modifiers/region.py +++ b/korman/properties/modifiers/region.py @@ -17,6 +17,9 @@ import bpy from bpy.props import * from PyHSPlasma import * +from ...exporter import ExportError +from ...helpers import TemporaryObject + from .base import PlasmaModifierProperties, PlasmaModifierLogicWiz from .physics import bounds_types @@ -140,3 +143,91 @@ class PlasmaPanicLinkRegion(PlasmaModifierProperties): @property def requires_actor(self): return True + + +class PlasmaSoftVolume(PlasmaModifierProperties): + pl_id = "softvolume" + + bl_category = "Region" + bl_label = "Soft Volume" + bl_description = "Soft-Boundary Region" + + # Advanced + use_nodes = BoolProperty(name="Use Nodes", + description="Make this a node-based Soft Volume", + default=False) + node_tree_name = StringProperty(name="Node Tree", + description="Node Tree detailing soft volume logic") + + # Basic + invert = BoolProperty(name="Invert", + description="Invert the soft region") + inside_strength = IntProperty(name="Inside", description="Strength inside the region", + subtype="PERCENTAGE", default=100, min=0, max=100) + outside_strength = IntProperty(name="Outside", description="Strength outside the region", + subtype="PERCENTAGE", default=0, min=0, max=100) + soft_distance = FloatProperty(name="Distance", description="Soft Distance", + default=0.0, min=0.0, max=500.0) + + def _apply_settings(self, sv): + sv.insideStrength = self.inside_strength / 100.0 + sv.outsideStrength = self.outside_strength / 100.0 + + def get_key(self, exporter, so=None): + """Fetches the key appropriate for this Soft Volume""" + if so is None: + so = exporter.mgr.find_create_object(plSceneObject, bl=self.id_data) + + if self.use_nodes: + output = self.node_tree.find_output("PlasmaSoftVolumeOutputNode") + if output is None: + raise ExportError("SoftVolume '{}' Node Tree '{}' has no output node!".format(self.key_name, self.node_tree)) + return output.get_key(exporter, so) + else: + pClass = plSoftVolumeInvert if self.invert else plSoftVolumeSimple + return exporter.mgr.find_create_key(pClass, bl=self.id_data, so=so) + + def export(self, exporter, bo, so): + if self.use_nodes: + self._export_sv_nodes(exporter, bo, so) + else: + self._export_convex_region(exporter, bo, so) + + def _export_convex_region(self, exporter, bo, so): + if bo.type != "MESH": + raise ExportError("SoftVolume '{}': Simple SoftVolumes can only be meshes!".format(bo.name)) + + # Grab the SoftVolume KO + sv = self.get_key(exporter, so).object + self._apply_settings(sv) + + # If "invert" was checked, we got a SoftVolumeInvert, but we need to make a Simple for the + # region data to be exported into.. + if isinstance(sv, plSoftVolumeInvert): + svSimple = exporter.mgr.find_create_object(plSoftVolumeSimple, bl=bo, so=so) + self._apply_settings(svSimple) + sv.addSubVolume(svSimple.key) + sv = svSimple + sv.softDist = self.soft_distance + + # Initialize the plVolumeIsect. Currently, we only support convex isects. If you want parallel + # isects from empties, be my guest... + with TemporaryObject(bo.to_mesh(bpy.context.scene, True, "RENDER", calc_tessface=False), bpy.data.meshes.remove) as mesh: + mesh.transform(bo.matrix_world) + + isect = plConvexIsect() + for i in mesh.vertices: + isect.addPlane(hsVector3(*i.normal), hsVector3(*i.co)) + sv.volume = isect + + def _export_sv_nodes(self, exporter, bo, so): + if self.node_tree_name not in exporter.node_trees_exported: + exporter.node_trees_exported.add(self.node_tree_name) + self.node_tree.export(exporter, bo, so) + + @property + def node_tree(self): + tree = bpy.data.node_groups.get(self.node_tree_name, None) + if tree is None: + raise ExportError("SoftVolume '{}': Node Tree '{}' does not exist!".format(self.key_name, self.node_tree_name)) + return tree diff --git a/korman/ui/modifiers/region.py b/korman/ui/modifiers/region.py index 6506eac..ea58627 100644 --- a/korman/ui/modifiers/region.py +++ b/korman/ui/modifiers/region.py @@ -13,6 +13,8 @@ # You should have received a copy of the GNU General Public License # along with Korman. If not, see . +import bpy + def footstep(modifier, layout, context): layout.prop(modifier, "bounds") layout.prop(modifier, "surface") @@ -21,3 +23,19 @@ def paniclink(modifier, layout, context): phys_mod = context.object.plasma_modifiers.collision layout.prop(phys_mod, "bounds") layout.prop(modifier, "play_anim") + +def softvolume(modifier, layout, context): + row = layout.row() + row.prop(modifier, "use_nodes", text="", icon="NODETREE") + if modifier.use_nodes: + row.prop_search(modifier, "node_tree_name", bpy.data, "node_groups") + else: + row.label("Simple Soft Volume") + + split = layout.split() + col = split.column() + col.prop(modifier, "inside_strength") + col.prop(modifier, "outside_strength") + col = split.column() + col.prop(modifier, "invert") + col.prop(modifier, "soft_distance")