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")