diff --git a/korman/exporter/mesh.py b/korman/exporter/mesh.py
index 0662b8b..d1a3c0f 100644
--- a/korman/exporter/mesh.py
+++ b/korman/exporter/mesh.py
@@ -264,7 +264,7 @@ class MeshConverter:
# Create the DrawInterface
if drawables:
- diface = self._mgr.add_object(pl=plDrawInterface, bl=bo)
+ diface = self._mgr.find_create_object(plDrawInterface, bl=bo)
for dspan_key, idx in drawables:
diface.addDrawable(dspan_key, idx)
diff --git a/korman/exporter/rtlight.py b/korman/exporter/rtlight.py
index 7412dbb..72dd35c 100644
--- a/korman/exporter/rtlight.py
+++ b/korman/exporter/rtlight.py
@@ -89,18 +89,11 @@ class LightConverter:
def _convert_sun_lamp(self, bl, pl):
print(" [DirectionalLightInfo '{}']".format(bl.name))
- def _create_light_key(self, bo, bl_light, so):
- try:
- xlate = _BL2PL[bl_light.type]
- return self.mgr.find_create_key(xlate, bl=bo, so=so)
- except LookupError:
- raise BlenderOptionNotSupported("Object ('{}') lamp type '{}'".format(bo.name, bl_light.type))
-
def export_rtlight(self, so, bo):
bl_light = bo.data
# The specifics be here...
- pl_light = self._create_light_key(bo, bl_light, so).object
+ pl_light = self.get_light_key(bo, bl_light, so).object
self._converter_funcs[bl_light.type](bl_light, pl_light)
# Light color nonsense
@@ -188,7 +181,7 @@ class LightConverter:
continue
# This is probably where PermaLight vs PermaProj should be sorted out...
- pl_light = self._create_light_key(obj, lamp, None)
+ pl_light = self.get_light_key(obj, lamp, None)
if self._is_projection_lamp(lamp):
print(" [{}] PermaProj '{}'".format(lamp.type, obj.name))
permaProj.append(pl_light)
@@ -200,6 +193,13 @@ class LightConverter:
return (permaLights, permaProjs)
+ def get_light_key(self, bo, bl_light, so):
+ try:
+ xlate = _BL2PL[bl_light.type]
+ return self.mgr.find_create_key(xlate, bl=bo, so=so)
+ except LookupError:
+ raise BlenderOptionNotSupported("Object ('{}') lamp type '{}'".format(bo.name, bl_light.type))
+
def _is_projection_lamp(self, bl_light):
for tex in bl_light.texture_slots:
if tex is None or tex.texture is None:
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/properties/modifiers/render.py b/korman/properties/modifiers/render.py
index 5071bc1..4fd49c9 100644
--- a/korman/properties/modifiers/render.py
+++ b/korman/properties/modifiers/render.py
@@ -172,6 +172,87 @@ class PlasmaViewFaceMod(PlasmaModifierProperties):
def requires_actor(self):
return True
+
+class PlasmaVisControl(PlasmaModifierProperties):
+ pl_id = "visregion"
+
+ bl_category = "Render"
+ bl_label = "Visibility Control"
+ bl_description = "Controls object visibility using VisRegions"
+
+ mode = EnumProperty(name="Mode",
+ description="Purpose of the VisRegion",
+ items=[("normal", "Normal", "Objects are only visible when the camera is inside this region"),
+ ("exclude", "Exclude", "Objects are only visible when the camera is outside this region"),
+ ("fx", "Special FX", "This is a list of objects used for special effects only")])
+ softvolume = StringProperty(name="Region",
+ description="Object defining the SoftVolume for this VisRegion")
+ replace_normal = BoolProperty(name="Hide Drawables",
+ description="Hides drawables attached to this region",
+ default=True)
+
+ def export(self, exporter, bo, so):
+ rgn = exporter.mgr.find_create_object(plVisRegion, bl=bo, so=so)
+ rgn.setProperty(plVisRegion.kReplaceNormal, self.replace_normal)
+
+ if self.mode == "fx":
+ rgn.setProperty(plVisRegion.kDisable, True)
+ else:
+ this_sv = bo.plasma_modifiers.softvolume
+ if this_sv.enabled:
+ print(" [VisRegion] I'm a SoftVolume myself :)")
+ rgn.region = this_sv.get_key(exporter, so)
+ else:
+ print(" [VisRegion] SoftVolume '{}'".format(self.softvolume))
+ sv_bo = bpy.data.objects.get(self.softvolume, None)
+ if sv_bo is None:
+ raise ExportError("'{}': Invalid object '{}' for VisControl soft volume".format(bo.name, self.softvolume))
+ sv = sv_bo.plasma_modifiers.softvolume
+ if not sv.enabled:
+ raise ExportError("'{}': '{}' is not a SoftVolume".format(bo.name, self.softvolume))
+ rgn.region = sv.get_key(exporter)
+ rgn.setProperty(plVisRegion.kIsNot, self.mode == "exclude")
+
+
+class VisRegion(bpy.types.PropertyGroup):
+ enabled = BoolProperty(default=True)
+ region_name = StringProperty(name="Control",
+ description="Object defining a Plasma Visibility Control")
+
+
+class PlasmaVisibilitySet(PlasmaModifierProperties):
+ pl_id = "visibility"
+
+ bl_category = "Render"
+ bl_label = "Visibility Set"
+ bl_description = "Defines areas where this object is visible"
+
+ regions = CollectionProperty(name="Visibility Regions",
+ type=VisRegion)
+ active_region_index = IntProperty(options={"HIDDEN"})
+
+ def export(self, exporter, bo, so):
+ if not self.regions:
+ # TODO: Log message about how this modifier is totally worthless
+ return
+
+ # Currently, this modifier is valid for meshes and lamps
+ if bo.type == "MESH":
+ diface = exporter.mgr.find_create_object(plDrawInterface, bl=bo, so=so)
+ addRegion = diface.addRegion
+ elif bo.type == "LAMP":
+ light = exporter.light.get_light_key(bo, bo.data, so)
+ addRegion = light.object.addVisRegion
+
+ for region in self.regions:
+ if not region.enabled:
+ continue
+ rgn_bo = bpy.data.objects.get(region.region_name, None)
+ if rgn_bo is None:
+ raise ExportError("{}: Invalid VisControl '{}' in VisSet modifier".format(bo.name, region.region_name))
+ addRegion(exporter.mgr.find_create_key(plVisRegion, bl=rgn_bo))
+
+
class PlasmaFollowMod(PlasmaModifierProperties):
pl_id = "followmod"
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")
diff --git a/korman/ui/modifiers/render.py b/korman/ui/modifiers/render.py
index eaf9bb4..0a0c956 100644
--- a/korman/ui/modifiers/render.py
+++ b/korman/ui/modifiers/render.py
@@ -56,6 +56,41 @@ def viewfacemod(modifier, layout, context):
col.enabled = modifier.offset
col.prop(modifier, "offset_coord")
+class VisRegionListUI(bpy.types.UIList):
+ def draw_item(self, context, layout, data, item, icon, active_data, active_property, index=0, flt_flag=0):
+ myIcon = "ERROR" if bpy.data.objects.get(item.region_name, None) is None else "OBJECT_DATA"
+ label = item.region_name if item.region_name else "[No Object Specified]"
+ layout.label(label, icon=myIcon)
+ layout.prop(item, "enabled", text="")
+
+
+def visibility(modifier, layout, context):
+ row = layout.row()
+ row.template_list("VisRegionListUI", "regions", modifier, "regions", modifier, "active_region_index",
+ rows=2, maxrows=3)
+ col = row.column(align=True)
+ op = col.operator("object.plasma_modifier_collection_add", icon="ZOOMIN", text="")
+ op.modifier = modifier.pl_id
+ op.collection = "regions"
+ op = col.operator("object.plasma_modifier_collection_remove", icon="ZOOMOUT", text="")
+ op.modifier = modifier.pl_id
+ op.collection = "regions"
+ op.index = modifier.active_region_index
+
+ if modifier.regions:
+ layout.prop_search(modifier.regions[modifier.active_region_index], "region_name", bpy.data, "objects")
+
+def visregion(modifier, layout, context):
+ layout.prop(modifier, "mode")
+
+ # Only allow SoftVolume spec if this is not an FX and this object is not an SV itself
+ sv = modifier.id_data.plasma_modifiers.softvolume
+ if modifier.mode != "fx" and not sv.enabled:
+ layout.prop_search(modifier, "softvolume", bpy.data, "objects")
+
+ # Other settings
+ layout.prop(modifier, "replace_normal")
+
def followmod(modifier, layout, context):
layout.row().prop(modifier, "follow_mode", expand=True)
layout.prop(modifier, "leader_type")