From 7d7af297e466c9211eb927660cffea388b9de12f Mon Sep 17 00:00:00 2001 From: Adam Johnson Date: Mon, 23 Nov 2015 23:33:31 -0500 Subject: [PATCH 1/2] 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") From c57407573ad167f59cc500e1823c5974ab5b3116 Mon Sep 17 00:00:00 2001 From: Adam Johnson Date: Sun, 17 Jan 2016 21:20:28 -0500 Subject: [PATCH 2/2] Implement VisRegions --- korman/exporter/mesh.py | 2 +- korman/exporter/rtlight.py | 18 +++--- korman/properties/modifiers/render.py | 81 +++++++++++++++++++++++++++ korman/ui/modifiers/render.py | 35 ++++++++++++ 4 files changed, 126 insertions(+), 10 deletions(-) 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/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/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")