From fedb7c91f56147159710746654eeec5d95ef5e1e Mon Sep 17 00:00:00 2001 From: Adam Johnson Date: Mon, 28 Sep 2020 20:53:59 -0400 Subject: [PATCH] Add "Blending" modifier and improve render level export. This matches us up more closely with what PlasmaMAX does and allows us to really fix x-raying effects by properly forcing objects into the correct render pass. --- korman/exporter/mesh.py | 46 +++++++++---- korman/idprops.py | 3 + korman/properties/modifiers/__init__.py | 4 ++ korman/properties/modifiers/base.py | 20 ++++++ korman/properties/modifiers/render.py | 92 +++++++++++++++++++++++++ korman/ui/modifiers/render.py | 30 ++++++++ 6 files changed, 181 insertions(+), 14 deletions(-) diff --git a/korman/exporter/mesh.py b/korman/exporter/mesh.py index cb7d388..5da7a62 100644 --- a/korman/exporter/mesh.py +++ b/korman/exporter/mesh.py @@ -40,16 +40,12 @@ class _RenderLevel: _MINOR_MASK = ((1 << _MAJOR_SHIFT) - 1) def __init__(self, bo, pass_index, blend_span=False): - self.level = 0 - if pass_index > 0: - self.major = self.MAJOR_FRAMEBUF - self.minor = pass_index * 4 + if blend_span: + self.level = self._determine_level(bo, blend_span) else: - self.major = self.MAJOR_BLEND if blend_span else self.MAJOR_OPAQUE - - # We use the blender material's pass index (which we stashed in the hsGMaterial) to increment - # the render pass, just like it says... - self.level += pass_index + self.level = 0 + # Gulp... Hope you know what you're doing... + self.minor += pass_index * 4 def __eq__(self, other): return self.level == other.level @@ -60,15 +56,38 @@ class _RenderLevel: def _get_major(self): return self.level >> self._MAJOR_SHIFT def _set_major(self, value): - self.level = ((value << self._MAJOR_SHIFT) & 0xFFFFFFFF) | self.minor + self.level = self._calc_level(value, self.minor) major = property(_get_major, _set_major) def _get_minor(self): return self.level & self._MINOR_MASK def _set_minor(self, value): - self.level = ((self.major << self._MAJOR_SHIFT) & 0xFFFFFFFF) | value + self.level = self._calc_level(self.major, value) minor = property(_get_minor, _set_minor) + def _calc_level(self, major : int, minor : int=0) -> int: + return ((major << self._MAJOR_SHIFT) & 0xFFFFFFFF) | minor + + def _determine_level(self, bo : bpy.types.Object, blend_span : bool) -> int: + mods = bo.plasma_modifiers + if mods.test_property("draw_framebuf"): + return self._calc_level(self.MAJOR_FRAMEBUF) + elif mods.test_property("draw_opaque"): + return self._calc_level(self.MAJOR_OPAQUE) + elif mods.test_property("draw_no_defer"): + blend_span = False + + blend_mod = mods.blend + if blend_mod.enabled and blend_mod.has_dependencies: + level = self._calc_level(self.MAJOR_FRAMEBUF) + for i in blend_mod.iter_dependencies(): + level = max(level, self._determine_level(i, blend_span)) + return level + 4 + elif blend_span: + return self._calc_level(self.MAJOR_BLEND) + else: + return self._calc_level(self.MAJOR_DEFAULT) + class _DrawableCriteria: def __init__(self, bo, geospan, pass_index): @@ -96,12 +115,12 @@ class _DrawableCriteria: def _face_sort_allowed(self, bo): # For now, only test the modifiers # This will need to be tweaked further for GUIs... - return not any((i.no_face_sort for i in bo.plasma_modifiers.modifiers)) + return not bo.plasma_modifiers.test_property("no_face_sort") def _span_sort_allowed(self, bo): # For now, only test the modifiers # This will need to be tweaked further for GUIs... - return not any((i.no_face_sort for i in bo.plasma_modifiers.modifiers)) + return not bo.plasma_modifiers.test_property("no_face_sort") @property def span_type(self): @@ -118,7 +137,6 @@ class _GeoData: self.vertices = [] - class _MeshManager: def __init__(self, report=None): if report is not None: diff --git a/korman/idprops.py b/korman/idprops.py index 66328d2..b7c55a6 100644 --- a/korman/idprops.py +++ b/korman/idprops.py @@ -127,6 +127,9 @@ def poll_animated_objects(self, value): def poll_camera_objects(self, value): return value.type == "CAMERA" +def poll_drawable_objects(self, value): + return value.type == "MESH" and any(value.data.materials) + def poll_empty_objects(self, value): return value.type == "EMPTY" diff --git a/korman/properties/modifiers/__init__.py b/korman/properties/modifiers/__init__.py index f4905d3..ba179df 100644 --- a/korman/properties/modifiers/__init__.py +++ b/korman/properties/modifiers/__init__.py @@ -66,6 +66,10 @@ class PlasmaModifiers(bpy.types.PropertyGroup): setattr(cls, i.pl_id, bpy.props.PointerProperty(type=i)) bpy.types.Object.plasma_modifiers = bpy.props.PointerProperty(type=cls) + def test_property(self, property : str) -> bool: + """Tests a property on all enabled Plasma modifiers""" + return any((getattr(i, property) for i in self.modifiers)) + class PlasmaModifierSpec(bpy.types.PropertyGroup): pass diff --git a/korman/properties/modifiers/base.py b/korman/properties/modifiers/base.py index 5f4ea34..0f52486 100644 --- a/korman/properties/modifiers/base.py +++ b/korman/properties/modifiers/base.py @@ -30,10 +30,30 @@ class PlasmaModifierProperties(bpy.types.PropertyGroup): def destroyed(self): pass + @property + def draw_opaque(self): + """Render geometry before the avatar""" + return False + + @property + def draw_framebuf(self): + """Render geometry after the avatar but before other blended geometry""" + return False + + @property + def draw_no_defer(self): + """Disallow geometry being sorted into a blending span""" + return False + @property def enabled(self): return self.display_order >= 0 + @property + def face_sort(self): + """Indicates that the geometry's faces should be sorted by the engine""" + return False + def harvest_actors(self): return () diff --git a/korman/properties/modifiers/render.py b/korman/properties/modifiers/render.py index 856840c..f49db3c 100644 --- a/korman/properties/modifiers/render.py +++ b/korman/properties/modifiers/render.py @@ -25,6 +25,98 @@ from ...exporter import utils from ...exporter.explosions import ExportError from ... import idprops +class PlasmaBlendOntoObject(bpy.types.PropertyGroup): + blend_onto = PointerProperty(name="Blend Onto", + description="Object to render first", + options=set(), + type=bpy.types.Object, + poll=idprops.poll_drawable_objects) + enabled = BoolProperty(name="Enabled", + default=True, + options=set()) + + +class PlasmaBlendMod(PlasmaModifierProperties): + pl_id = "blend" + + bl_category = "Render" + bl_label = "Blending" + bl_description = "Advanced Blending Options" + + render_level = EnumProperty(name="Render Pass", + description="Suggested render pass for this object.", + items=[("AUTO", "(Auto)", "Let Korman decide when to render this object."), + ("OPAQUE", "Before Avatar", "Prefer for the object to draw before the avatar."), + ("FRAMEBUF", "Frame Buffer", "Prefer for the object to draw after the avatar but before other blended objects."), + ("BLEND", "Blended", "Prefer for the object to draw after most other geometry in the blended pass.")], + options=set()) + sort_faces = EnumProperty(name="Sort Faces", + description="", + items=[("AUTO", "(Auto)", "Let Korman decide if faces should be sorted."), + ("ALWAYS", "Always", "Force the object's faces to be sorted."), + ("NEVER", "Never", "Force the object's faces to never be sorted.")], + options=set()) + + dependencies = CollectionProperty(type=PlasmaBlendOntoObject) + active_dependency_index = IntProperty(options={"HIDDEN"}) + + def export(self, exporter, bo, so): + # What'er you lookin at? + pass + + @property + def draw_opaque(self): + return self.render_level == "OPAQUE" + + @property + def draw_framebuf(self): + return self.render_level == "FRAMEBUF" + + @property + def draw_no_defer(self): + return self.render_level != "BLEND" + + @property + def face_sort(self): + return self.sort_faces == "ALWAYS" + + @property + def no_face_sort(self): + return self.sort_faces == "NEVER" + + @property + def has_dependencies(self): + return bool(self.dependencies) + + @property + def has_circular_dependency(self): + return self._check_circular_dependency() + + def _check_circular_dependency(self, objects=None): + if objects is None: + objects = set() + elif self.name in objects: + return True + objects.add(self.name) + + for i in self.iter_dependencies(): + # New deep copy of the set for each dependency, so an object can be reused as a + # dependant's dependant. + this_branch = set(objects) + sub_mod = i.plasma_modifiers.blend + if sub_mod.enabled and sub_mod._check_circular_dependency(this_branch): + return True + return False + + def iter_dependencies(self): + for i in (j.blend_onto for j in self.dependencies if j.blend_onto is not None and j.enabled): + yield i + + def sanity_check(self): + if self.has_circular_dependency: + raise ExportError("'{}': Circular Render Dependency detected!".format(self.name)) + + class PlasmaDecalManagerRef(bpy.types.PropertyGroup): enabled = BoolProperty(name="Enabled", default=True, diff --git a/korman/ui/modifiers/render.py b/korman/ui/modifiers/render.py index 64d3c9b..7c6dfa6 100644 --- a/korman/ui/modifiers/render.py +++ b/korman/ui/modifiers/render.py @@ -18,6 +18,36 @@ import bpy from .. import ui_list from ...exporter.mesh import _VERTEX_COLOR_LAYERS +class BlendOntoListUI(bpy.types.UIList): + def draw_item(self, context, layout, data, item, icon, active_data, active_property, index=0, flt_flag=0): + if item.blend_onto is None: + layout.label("[No Object Specified]", icon="ERROR") + else: + layout.label(item.blend_onto.name, icon="OBJECT_DATA") + layout.prop(item, "enabled", text="") + + +def blend(modifier, layout, context): + # Warn if there are render dependencies and a manual render level specification -- this + # could lead to unpredictable results. + layout.alert = modifier.render_level != "AUTO" and bool(modifier.dependencies) + layout.prop(modifier, "render_level") + layout.alert = False + layout.prop(modifier, "sort_faces") + + layout.separator() + layout.label("Render Dependencies:") + ui_list.draw_modifier_list(layout, "BlendOntoListUI", modifier, "dependencies", + "active_dependency_index", rows=2, maxrows=4) + try: + dependency_ref = modifier.dependencies[modifier.active_dependency_index] + except: + pass + else: + layout.alert = dependency_ref.blend_onto is None + layout.prop(dependency_ref, "blend_onto") + + class DecalMgrListUI(bpy.types.UIList): def draw_item(self, context, layout, data, item, icon, active_data, active_property, index=0, flt_flag=0): if item.name: