From e2134d16ad5f7cad147aa25130fe1ada8bc7c696 Mon Sep 17 00:00:00 2001 From: Adam Johnson Date: Thu, 19 Dec 2019 19:19:00 -0500 Subject: [PATCH 1/5] Implement basic dynamic decals. The code has some rudimentary support for all kinds of decals, but the main focus of this changeset is unconditional footprints and water ripples. --- korman/exporter/convert.py | 2 + korman/exporter/decal.py | 141 ++++++++++++++++++++++++++ korman/exporter/material.py | 83 ++++++++++++++- korman/properties/modifiers/render.py | 33 ++++++ korman/properties/prop_scene.py | 64 ++++++++++++ korman/ui/__init__.py | 1 + korman/ui/modifiers/render.py | 23 +++++ korman/ui/ui_scene.py | 72 +++++++++++++ 8 files changed, 414 insertions(+), 5 deletions(-) create mode 100644 korman/exporter/decal.py create mode 100644 korman/ui/ui_scene.py diff --git a/korman/exporter/convert.py b/korman/exporter/convert.py index 8b4f1be..ffcafe2 100644 --- a/korman/exporter/convert.py +++ b/korman/exporter/convert.py @@ -21,6 +21,7 @@ import time from . import animation from . import camera +from . import decal from . import explosions from . import etlight from . import image @@ -54,6 +55,7 @@ class Exporter: self.camera = camera.CameraConverter(self) self.image = image.ImageCache(self) self.locman = locman.LocalizationConverter(self) + self.decal = decal.DecalConverter(self) # Step 0.8: Init the progress mgr self.mesh.add_progress_presteps(self.report) diff --git a/korman/exporter/decal.py b/korman/exporter/decal.py new file mode 100644 index 0000000..ab05e23 --- /dev/null +++ b/korman/exporter/decal.py @@ -0,0 +1,141 @@ +# 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 collections import defaultdict +from PyHSPlasma import * +import weakref + +from ..exporter.explosions import ExportError + +def _get_puddle_class(exporter, name, vs): + if vs: + # sigh... thou shalt not... + exporter.report.warn("'{}': Cannot use 'Water Ripple (Shallow) on a waveset--forcing to 'Water Ripple (Deep)", name) + return plDynaRippleVSMgr + return plDynaPuddleMgr + +def _get_footprint_class(exporter, name, vs): + if vs: + raise ExportError("'{}': Footprints cannot be attached to wavesets", name) + return plDynaFootMgr + +class DecalConverter: + _decal_lookup = { + "footprint": _get_footprint_class, + "puddle": _get_puddle_class, + "ripple": lambda e, name, vs: plDynaRippleVSMgr if vs else plDynaRippleMgr, + } + + def __init__(self, exporter): + self._decal_managers = defaultdict(list) + self._exporter = weakref.ref(exporter) + + def add_dynamic_decal_receiver(self, so, decal_name): + # One decal manager in Blender can map to many Plasma decal managers. + # The case we care about: a single water decal exporting to multiple DynaDecalMgrs + # eg two wavesets (two mgrs) and two water planes (one mgr) + # We don't care about: DynaDecalMgrs in another page. + decal_mgrs, so_key = self._decal_managers.get(decal_name), so.key + if decal_mgrs is None: + raise ExportError("'{}': Invalid decal manager '{}'", so_key.name, decal_name) + + # If we are waveset water, then we can only have one target... + waveset_id = plFactory.ClassIndex("plWaveSet7") + waveset = next((i for i in so.modifiers if i.type == waveset_id), None) + + so_loc = so_key.location + for key, decal_mgr in ((i, i.object) for i in decal_mgrs): + if key.location == so_loc and getattr(decal_mgr, "waveSet", None) == waveset: + decal_mgr.addTarget(so_key) + + def generate_dynamic_decal(self, bo, decal_name): + decal = next((i for i in bpy.context.scene.plasma_scene.decal_managers if i.name == decal_name), None) + if decal is None: + raise ExportError("'{}': Invalid decal manager '{}'", bo.name, decal_name) + + exporter = self._exporter() + decal_type = decal.decal_type + is_waveset = bo.plasma_modifiers.water_basic.enabled + pClass = self._decal_lookup[decal_type](exporter, decal_name, is_waveset) + + # DynaDecal Managers generate geometry at runtime, so we need to share them as much as + # possible. However, it is best to keep things page local. Furthermore, wavesets cannot + # share decal managers due to vertex shaders being used. + name = "{}_{}".format(decal_name, bo.name) if is_waveset else decal_name + decal_mgr = exporter.mgr.find_object(pClass, bl=bo, name=name) + if decal_mgr is None: + self._report.msg("Exporing decal manager '{}' to '{}'", decal_name, name, indent=2) + + decal_mgr = exporter.mgr.add_object(pClass, bl=bo, name=name) + self._decal_managers[decal_name].append(decal_mgr.key) + + # Certain decals are required to be squares + if decal_type in {"footprint", "wake"}: + length, width = decal.length / 100.0, decal.width / 100.0 + else: + length = max(decal.length, decal.width) / 100.0 + width = max(decal.length, decal.width) / 100.0 + + image = decal.image + if image is None: + raise ExportError("'{}': decal manager '{}' has no image set", bo.name, decal_name) + + blend = getattr(hsGMatState, decal.blend) + mats = exporter.mesh.material.export_print_materials(bo, image, name, blend) + decal_mgr.matPreShade, decal_mgr.matRTShade = mats + + # Hardwired values from PlasmaMAX + decal_mgr.maxNumVerts = 1000 + decal_mgr.maxNumIdx = 1000 + decal_mgr.intensity = decal.intensity / 100.0 + decal_mgr.gridSizeU = 2.5 + decal_mgr.gridSizeV = 2.5 + decal_mgr.scale = hsVector3(length, width, 1.0) + + # Hardwired calculations from PlasmaMAX + if decal_type in {"footprint", "bullet"}: + decal_mgr.rampEnd = 0.1 + decal_mgr.decayStart = decal.life_span - (decal.life_span * 0.25) + decal_mgr.lifeSpan = decal.life_span + elif decal_type in {"puddle", "ripple", "torpedo", "wake"}: + decal_mgr.rampEnd = 0.25 + life_span = decal.life_span if decal_type == "torpedo" else length / 2.0 + decal_mgr.decayStart = life_span * 0.8 + decal_mgr.lifeSpan = life_span + else: + raise RuntimeError() + + # UV Animations are hardcoded in PlasmaMAX. Any reason why we should expose this? + # I can't think of any presently... Note testing the final instance instead of the + # artist setting in case that gets overridden (puddle -> ripple) + if isinstance(decal_mgr, plDynaPuddleMgr): + decal_mgr.initUVW = hsVector3(5.0, 5.0, 5.0) + decal_mgr.finalUVW = hsVector3(1.0, 1.0, 1.0) + elif isinstance(decal_mgr, plDynaRippleMgr): + # wakes, torpedos, and ripples... + decal_mgr.initUVW = hsVector3(3.0, 3.0, 3.0) + decal_mgr.finalUVW = hsVector3(1.0, 1.0, 1.0) + + if isinstance(decal_mgr, (plDynaRippleVSMgr, plDynaTorpedoVSMgr)): + decal_mgr.waveSet = exporter.mgr.find_create_key(plWaveSet7, bl=bo) + + @property + def _mgr(self): + return self._exporter().mgr + + @property + def _report(self): + return self._exporter().report diff --git a/korman/exporter/material.py b/korman/exporter/material.py index 7304b4b..a029cf3 100644 --- a/korman/exporter/material.py +++ b/korman/exporter/material.py @@ -90,6 +90,7 @@ class _Texture: self.ephemeral = kwargs.get("ephemeral", False) self.image = image self.tag = kwargs.get("tag", None) + self.name = kwargs.get("name", image.name) def __eq__(self, other): if not isinstance(other, _Texture): @@ -106,17 +107,17 @@ class _Texture: def __str__(self): if self.extension is None: - name = self.image.name + name = self.name else: - name = str(Path(self.image.name).with_suffix(".{}".format(self.extension))) + name = str(Path(self.name).with_suffix(".{}".format(self.extension))) if self.calc_alpha: - name = "ALPHAGEN_{}".format(name) + name = "ALPHAGEN_{}".format(self.name) if self.is_detail_map: name = "DETAILGEN_{}-{}-{}-{}-{}_{}".format(self._DETAIL_BLEND[self.detail_blend], self.detail_fade_start, self.detail_fade_stop, self.detail_opacity_start, self.detail_opacity_stop, - name) + self.name) return name def _update(self, other): @@ -260,6 +261,78 @@ class MaterialConverter: # Looks like we're done... return hsgmat.key + def export_print_materials(self, bo, image, name, blend): + """Exports dynamic decal print material(s)""" + + def make_print_material(name): + layer = self._mgr.add_object(plLayer, bl=bo, name=name) + layer.state.blendFlags = blend + layer.state.clampFlags = hsGMatState.kClampTexture + layer.state.ZFlags = hsGMatState.kZNoZWrite | hsGMatState.kZIncLayer + layer.ambient = hsColorRGBA(0.0, 0.0, 0.0, 1.0) + layer.preshade = hsColorRGBA(0.0, 0.0, 0.0, 1.0) + layer.runtime = hsColorRGBA(1.0, 1.0, 1.0, 1.0) + self.export_prepared_image(name=image_name, image=image, alpha_type=image_alpha, + owner=layer, allowed_formats={"DDS"}, indent=4) + material = self._mgr.add_object(hsGMaterial, bl=bo, name=name) + material.addLayer(layer.key) + return material, layer + + want_preshade = blend == hsGMatState.kBlendAlpha + + image_alpha = self._test_image_alpha(image) + if image_alpha == TextureAlpha.opaque and want_preshade: + self._report.warn("Using an opaque texture with alpha blending -- this may look bad") + + # Non-alpha blendmodes absolutely cannot have an alpha channel. Period. Nada. + # You can't even filter it out with blend flags. We'll try to mitigate the damage by + # exporting a DXT1 version. As of right now, opaque vs on_off does nothing, so we still + # get some turd-alpha data. + if image_alpha == TextureAlpha.full and not want_preshade: + self._report.warn("Using an alpha texture with a non-alpha blend mode -- this may look bad", indent=3) + image_alpha = TextureAlpha.opaque + image_name = "DECALPRINT_{}".format(image.name) + else: + image_name = image.name + + # Check to see if we have already processed this print material... + rtname = "DECALPRINT_{}".format(name) + rt_key = self._mgr.find_key(hsGMaterial, bl=bo, name=rtname) + if want_preshade: + prename = "DECALPRINT_{}_AH".format(name) + pre_key = self._mgr.find_key(hsGMaterial, bl=bo, name=prename) + else: + pre_key = None + if rt_key or pre_key: + return pre_key, rt_key + + self._report.msg("Exporting Print Material '{}'", rtname, indent=3) + rt_material, rt_layer = make_print_material(rtname) + if blend == hsGMatState.kBlendMult: + rt_layer.state.blendFlags |= hsGMatState.kBlendInvertFinalColor + rt_key = rt_material.key + + if want_preshade: + self._report.msg("Exporting Print Material '{}'", prename, indent=3) + pre_material, pre_layer = make_print_material(prename) + pre_material.compFlags |= hsGMaterial.kCompNeedsBlendChannel + pre_layer.state.miscFlags |= hsGMatState.kMiscBindNext | hsGMatState.kMiscRestartPassHere + pre_layer.preshade = hsColorRGBA(1.0, 1.0, 1.0, 1.0) + + blend_layer = self._mgr.add_object(plLayer, bl=bo, name="{}_AlphaBlend".format(rtname)) + blend_layer.state.blendFlags = hsGMatState.kBlendAlpha | hsGMatState.kBlendNoTexColor | \ + hsGMatState.kBlendAlphaMult + blend_layer.state.clampFlags = hsGMatState.kClampTexture + blend_layer.state.ZFlags = hsGMatState.kZNoZWrite + blend_layer.ambient = hsColorRGBA(1.0, 1.0, 1.0, 1.0) + pre_material.addLayer(blend_layer.key) + self.export_alpha_blend("LINEAR", "HORIZONTAL", owner=blend_layer, indent=4) + + pre_key = pre_material.key + else: + pre_key = None + return pre_key, rt_key + def export_waveset_material(self, bo, bm): self._report.msg("Exporting WaveSet Material '{}'", bm.name, indent=1) @@ -818,7 +891,7 @@ class MaterialConverter: image.pixels = pixels self.export_prepared_image(image=image, owner=owner, allowed_formats={"BMP"}, - alpha_type=TextureAlpha.full, indent=2, ephemeral=True) + alpha_type=TextureAlpha.full, indent=indent, ephemeral=True) def export_prepared_image(self, **kwargs): """This exports an externally prepared image and an optional owning layer. diff --git a/korman/properties/modifiers/render.py b/korman/properties/modifiers/render.py index befec0d..0406855 100644 --- a/korman/properties/modifiers/render.py +++ b/korman/properties/modifiers/render.py @@ -16,6 +16,7 @@ import bpy from bpy.props import * +import functools from PyHSPlasma import * from .base import PlasmaModifierProperties, PlasmaModifierUpgradable @@ -24,6 +25,38 @@ from ...exporter import utils from ...exporter.explosions import ExportError from ... import idprops +class PlasmaDecalManagerRef(bpy.types.PropertyGroup): + enabled = BoolProperty(name="Enabled", + default=True, + options=set()) + + name = StringProperty(name="Decal Name", + options=set()) + + +class PlasmaDecalReceiveMod(PlasmaModifierProperties): + pl_id = "decal_receive" + + bl_category = "Render" + bl_label = "Receive Decal" + bl_description = "Allows this object to receive dynamic decals" + + managers = CollectionProperty(type=PlasmaDecalManagerRef) + active_manager_index = IntProperty(options={"HIDDEN"}) + + def _iter_decals(self, func): + for decal_ref in self.managers: + if decal_ref.enabled: + func(decal_ref.name) + + def export(self, exporter, bo, so): + f = functools.partial(exporter.decal.generate_dynamic_decal, bo) + self._iter_decals(f) + + def post_export(self, exporter, bo, so): + f = functools.partial(exporter.decal.add_dynamic_decal_receiver, so) + self._iter_decals(f) + class PlasmaFadeMod(PlasmaModifierProperties): pl_id = "fademod" diff --git a/korman/properties/prop_scene.py b/korman/properties/prop_scene.py index 36d0675..6d9dc76 100644 --- a/korman/properties/prop_scene.py +++ b/korman/properties/prop_scene.py @@ -41,10 +41,74 @@ class PlasmaBakePass(bpy.types.PropertyGroup): default=((True,) * _NUM_RENDER_LAYERS)) +class PlasmaDecalManager(bpy.types.PropertyGroup): + def _get_display_name(self): + return self.name + def _set_display_name(self, value): + prev_value = self.name + for i in bpy.data.objects: + decal_receive = i.plasma_modifiers.decal_receive + for j in decal_receive.managers: + if j.name == prev_value: + j.name = value + self.name = value + + name = StringProperty(name="Decal Name", + options=set()) + display_name = StringProperty(name="Display Name", + get=_get_display_name, + set=_set_display_name, + options=set()) + + decal_type = EnumProperty(name="Decal Type", + description="", + items=[("footprint", "Footprint", ""), + ("puddle", "Water Ripple (Shallow)", ""), + ("ripple", "Water Ripple (Deep)", "")], + default="footprint", + options=set()) + image = PointerProperty(name="Image", + description="", + type=bpy.types.Image, + options=set()) + blend = EnumProperty(name="Blend Mode", + description="", + items=[("kBlendAdd", "Add", ""), + ("kBlendAlpha", "Alpha", ""), + ("kBlendMADD", "Brighten", ""), + ("kBlendMult", "Multiply", "")], + default="kBlendAlpha", + options=set()) + + length = IntProperty(name="Length", + description="", + subtype="PERCENTAGE", + min=0, soft_min=25, soft_max=400, default=100, + options=set()) + width = IntProperty(name="Width", + description="", + subtype="PERCENTAGE", + min=0, soft_min=25, soft_max=400, default=100, + options=set()) + intensity = IntProperty(name="Intensity", + description="", + subtype="PERCENTAGE", + min=0, soft_max=100, default=100, + options=set()) + life_span = FloatProperty(name="Life Span", + description="", + subtype="TIME", unit="TIME", + min=0.0, soft_max=300.0, default=30.0, + options=set()) + + class PlasmaScene(bpy.types.PropertyGroup): bake_passes = CollectionProperty(type=PlasmaBakePass) active_pass_index = IntProperty(options={"HIDDEN"}) + decal_managers = CollectionProperty(type=PlasmaDecalManager) + active_decal_index = IntProperty(options={"HIDDEN"}) + modifier_copy_object = PointerProperty(name="INTERNAL: Object to copy modifiers from", options={"HIDDEN", "SKIP_SAVE"}, type=bpy.types.Object) diff --git a/korman/ui/__init__.py b/korman/ui/__init__.py index a292060..5f0ddca 100644 --- a/korman/ui/__init__.py +++ b/korman/ui/__init__.py @@ -21,6 +21,7 @@ from .ui_menus import * from .ui_modifiers import * from .ui_object import * from .ui_render_layer import * +from .ui_scene import * from .ui_text import * from .ui_texture import * from .ui_toolbox import * diff --git a/korman/ui/modifiers/render.py b/korman/ui/modifiers/render.py index 482c412..19afc84 100644 --- a/korman/ui/modifiers/render.py +++ b/korman/ui/modifiers/render.py @@ -18,6 +18,29 @@ import bpy from .. import ui_list from ...exporter.mesh import _VERTEX_COLOR_LAYERS +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: + layout.label(item.name) + layout.prop(item, "enabled", text="") + else: + layout.label("[Empty]") + + +def decal_receive(modifier, layout, context): + ui_list.draw_modifier_list(layout, "DecalMgrListUI", modifier, "managers", + "active_manager_index", rows=2, maxrows=3) + try: + mgr_ref = modifier.managers[modifier.active_manager_index] + except: + pass + else: + scene = context.scene.plasma_scene + decal_mgr = next((i for i in scene.decal_managers if i.display_name == mgr_ref), None) + + layout.alert = decal_mgr is None + layout.prop_search(mgr_ref, "name", scene, "decal_managers", icon="NONE") + def fademod(modifier, layout, context): layout.prop(modifier, "fader_type") diff --git a/korman/ui/ui_scene.py b/korman/ui/ui_scene.py new file mode 100644 index 0000000..efcac8e --- /dev/null +++ b/korman/ui/ui_scene.py @@ -0,0 +1,72 @@ +# 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 +import functools +from . import ui_list + +class SceneButtonsPanel: + bl_space_type = "PROPERTIES" + bl_region_type = "WINDOW" + bl_context = "scene" + + @classmethod + def poll(cls, context): + return context.scene.render.engine == "PLASMA_GAME" + + +class DecalManagerListUI(bpy.types.UIList): + def draw_item(self, context, layout, data, item, icon, active_data, active_property, index=0, flt_flag=0): + layout.prop(item, "display_name", emboss=False, text="") + + +class PlasmaDecalManagersPanel(SceneButtonsPanel, bpy.types.Panel): + bl_label = "Plasma Decal Managers" + + def draw(self, context): + layout, scene = self.layout, context.scene.plasma_scene + + ui_list.draw_list(layout, "DecalManagerListUI", "scene", scene, "decal_managers", + "active_decal_index", name_prefix="Decal", name_prop="display_name", + rows=3) + + try: + decal_mgr = scene.decal_managers[scene.active_decal_index] + except: + pass + else: + box = layout.box().column() + + box.prop(decal_mgr, "decal_type") + box.alert = decal_mgr.image is None + box.prop(decal_mgr, "image") + box.alert = False + box.prop(decal_mgr, "blend") + box.separator() + + split = box.split() + col = split.column(align=True) + col.label("Scale:") + col.alert = decal_mgr.decal_type in {"ripple", "puddle", "bullet", "torpedo"} \ + and decal_mgr.length != decal_mgr.width + col.prop(decal_mgr, "length") + col.prop(decal_mgr, "width") + + col = split.column() + col.label("Draw Settings:") + col.prop(decal_mgr, "intensity") + sub = col.row() + sub.active = decal_mgr.decal_type in {"footprint", "bullet", "torpedo"} + sub.prop(decal_mgr, "life_span") From dd467547d2114365894a8add744691f977861fc0 Mon Sep 17 00:00:00 2001 From: Adam Johnson Date: Thu, 19 Dec 2019 19:20:09 -0500 Subject: [PATCH 2/5] Improve blendspan criteria stuff. This allows blend textures to be used for mesh transparency without having to fiddle around too much. It was also a stab at fixing some regressions in the MOUL engine around decals, but that bit failed, sadly. --- korman/exporter/material.py | 9 +++++++-- korman/exporter/mesh.py | 29 ++++++++++++++++++--------- korman/properties/modifiers/base.py | 14 ++++++------- korman/properties/modifiers/render.py | 4 ---- 4 files changed, 34 insertions(+), 22 deletions(-) diff --git a/korman/exporter/material.py b/korman/exporter/material.py index a029cf3..eac3fd4 100644 --- a/korman/exporter/material.py +++ b/korman/exporter/material.py @@ -429,8 +429,6 @@ class MaterialConverter: if canStencil: hsgmat.compFlags |= hsGMaterial.kCompNeedsBlendChannel state.blendFlags |= hsGMatState.kBlendAlpha | hsGMatState.kBlendAlphaMult | hsGMatState.kBlendNoTexColor - if slot.texture.type == "BLEND": - state.clampFlags |= hsGMatState.kClampTexture state.ZFlags |= hsGMatState.kZNoZWrite layer.ambient = hsColorRGBA(1.0, 1.0, 1.0, 1.0) elif blend_flags: @@ -453,6 +451,8 @@ class MaterialConverter: layer.specularPower = 1.0 texture = slot.texture + if texture.type == "BLEND": + hsgmat.compFlags |= hsGMaterial.kCompNeedsBlendChannel # Apply custom layer properties wantBumpmap = bm is not None and slot.use_map_normal @@ -788,6 +788,11 @@ class MaterialConverter: pass def _export_texture_type_blend(self, bo, layer, slot): + state = layer.state + state.blendFlags |= hsGMatState.kBlendAlpha | hsGMatState.kBlendAlphaMult | hsGMatState.kBlendNoTexColor + state.clampFlags |= hsGMatState.kClampTexture + state.ZFlags |= hsGMatState.kZNoZWrite + # This has been separated out because other things may need alpha blend textures. texture = slot.texture self.export_alpha_blend(texture.progression, texture.use_flip_axis, layer) diff --git a/korman/exporter/mesh.py b/korman/exporter/mesh.py index 62e3b24..dd4ad5e 100644 --- a/korman/exporter/mesh.py +++ b/korman/exporter/mesh.py @@ -39,11 +39,13 @@ class _RenderLevel: _MAJOR_SHIFT = 28 _MINOR_MASK = ((1 << _MAJOR_SHIFT) - 1) - def __init__(self, bo, hsgmat, pass_index, blendSpan=False): + def __init__(self, bo, hsgmat, pass_index, blend_span=False): self.level = 0 - - if blendSpan: - self.major = self.MAJOR_DEFAULT + if pass_index > 0: + self.major = self.MAJOR_FRAMEBUF + self.minor = pass_index * 4 + 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... @@ -74,11 +76,10 @@ class _DrawableCriteria: self.criteria = 0 if self.blend_span: - for mod in bo.plasma_modifiers.modifiers: - if mod.requires_face_sort: - self.criteria |= plDrawable.kCritSortFaces - if mod.requires_span_sort: - self.criteria |= plDrawable.kCritSortSpans + if self._face_sort_allowed(bo): + self.criteria |= plDrawable.kCritSortFaces + if self._span_sort_allowed(bo): + self.criteria |= plDrawable.kCritSortSpans self.render_level = _RenderLevel(bo, hsgmat, pass_index, self.blend_span) def __eq__(self, other): @@ -92,6 +93,16 @@ class _DrawableCriteria: def __hash__(self): return hash(self.render_level) ^ hash(self.blend_span) ^ hash(self.criteria) + 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)) + + 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)) + @property def span_type(self): if self.blend_span: diff --git a/korman/properties/modifiers/base.py b/korman/properties/modifiers/base.py index 987e1de..dd4a77a 100644 --- a/korman/properties/modifiers/base.py +++ b/korman/properties/modifiers/base.py @@ -37,19 +37,19 @@ class PlasmaModifierProperties(bpy.types.PropertyGroup): return self.id_data.name @property - def requires_actor(self): - """Indicates if this modifier requires the object to be a movable actor""" + def no_face_sort(self): + """Indicates that the geometry's faces should never be sorted by the engine""" return False @property - def requires_face_sort(self): - """Indicates that the geometry's faces must be sorted by the engine""" + def no_span_sort(self): + """Indicates that the geometry's Spans should never be sorted with those from other + Drawables that will render in the same pass""" return False @property - def requires_span_sort(self): - """Indicates that the geometry's Spans must be sorted with those from other Drawables that - will render in the same pass""" + def requires_actor(self): + """Indicates if this modifier requires the object to be a movable actor""" return False # Guess what? diff --git a/korman/properties/modifiers/render.py b/korman/properties/modifiers/render.py index 0406855..1827923 100644 --- a/korman/properties/modifiers/render.py +++ b/korman/properties/modifiers/render.py @@ -114,10 +114,6 @@ class PlasmaFadeMod(PlasmaModifierProperties): mod.farOpaq = self.far_opaq mod.farTrans = self.far_trans - @property - def requires_span_sort(self): - return True - class PlasmaFollowMod(idprops.IDPropObjectMixin, PlasmaModifierProperties): pl_id = "followmod" From 211a9dafee80afba7a4edc2232542b34bf9874b7 Mon Sep 17 00:00:00 2001 From: Adam Johnson Date: Fri, 20 Dec 2019 20:14:51 -0500 Subject: [PATCH 3/5] Implement decal printing. This allows for objects to print decals at runtime, like the baskets in Eder Gira. Also, the same functionality can be hijacked for coincident objects to exist as decals. This is basically a shortcut for adding hack Z-flags to the base layer of a decal material. --- korman/exporter/decal.py | 23 +++++++++ korman/exporter/material.py | 24 +++++---- korman/properties/modifiers/base.py | 5 ++ korman/properties/modifiers/render.py | 74 ++++++++++++++++++++++++--- korman/properties/modifiers/water.py | 4 ++ korman/properties/prop_scene.py | 4 +- korman/ui/modifiers/render.py | 26 ++++++++++ 7 files changed, 142 insertions(+), 18 deletions(-) diff --git a/korman/exporter/decal.py b/korman/exporter/decal.py index ab05e23..33461a1 100644 --- a/korman/exporter/decal.py +++ b/korman/exporter/decal.py @@ -61,6 +61,29 @@ class DecalConverter: if key.location == so_loc and getattr(decal_mgr, "waveSet", None) == waveset: decal_mgr.addTarget(so_key) + def export_active_print_shape(self, print_shape, decal_name): + decal_mgrs = self._decal_managers.get(decal_name) + if decal_mgrs is None: + raise ExportError("'{}': Invalid decal manager '{}'", print_shape.key.name, decal_name) + for i in decal_mgrs: + print_shape.addDecalMgr(i) + + def export_static_decal(self, bo): + mat_mgr = self._exporter().mesh.material + mat_keys = mat_mgr.get_materials(bo) + if not mat_keys: + raise ExportError("'{}': Cannot print decal onto object with no materials", bo.name) + + zFlags = hsGMatState.kZIncLayer | hsGMatState.kZNoZWrite + for material in (i.object for i in mat_keys): + # Only useful in a debugging context + material.compFlags |= hsGMaterial.kCompDecal + + # zFlags should only be applied to the material's base layer + # note: changing blend flags is unsafe here -- so don't even think about it! + layer = mat_mgr.get_base_layer(material) + layer.state.ZFlags |= zFlags + def generate_dynamic_decal(self, bo, decal_name): decal = next((i for i in bpy.context.scene.plasma_scene.decal_managers if i.name == decal_name), None) if decal is None: diff --git a/korman/exporter/material.py b/korman/exporter/material.py index eac3fd4..b56ea8b 100644 --- a/korman/exporter/material.py +++ b/korman/exporter/material.py @@ -180,9 +180,9 @@ class MaterialConverter: """Exports a Blender Material as an hsGMaterial""" # Sometimes, a material might need to be single-use. Right now, the most apparent example - # of that situation is when a lightmap image is baked. Wavesets are in the same boat, but - # that's a special case as of the writing of this code. - single_user = self._requires_single_user_material(bo, bm) + # of that situation is when a lightmap image is baked. There are others, but as of right now, + # it can all be determined by what mods are attached. + single_user = any((i.copy_material for i in bo.plasma_modifiers.modifiers)) if single_user: mat_name = "{}_AutoSingle".format(bm.name) if bo.name == bm.name else "{}_{}".format(bo.name, bm.name) self._report.msg("Exporting Material '{}' as single user '{}'", bm.name, mat_name, indent=1) @@ -1151,6 +1151,16 @@ class MaterialConverter: def get_materials(self, bo): return self._obj2mat.get(bo, []) + def get_base_layer(self, hsgmat): + try: + layer = hsgmat.layers[0].object + except IndexError: + return None + else: + while layer.underLay is not None: + layer = layer.underLay.object + return layer + def get_bump_layer(self, bo): return self._bump_mats.get(bo, None) @@ -1210,14 +1220,6 @@ class MaterialConverter: def _report(self): return self._exporter().report - def _requires_single_user_material(self, bo, bm): - modifiers = bo.plasma_modifiers - if modifiers.lightmap.bake_lightmap: - return True - if modifiers.water_basic.enabled: - return True - return False - def _test_image_alpha(self, image): """Tests to see if this image has any alpha data""" diff --git a/korman/properties/modifiers/base.py b/korman/properties/modifiers/base.py index dd4a77a..0d2049d 100644 --- a/korman/properties/modifiers/base.py +++ b/korman/properties/modifiers/base.py @@ -19,6 +19,11 @@ from bpy.props import * from contextlib import contextmanager class PlasmaModifierProperties(bpy.types.PropertyGroup): + @property + def copy_material(self): + """Materials MUST be single-user""" + return False + def created(self): pass diff --git a/korman/properties/modifiers/render.py b/korman/properties/modifiers/render.py index 1827923..856840c 100644 --- a/korman/properties/modifiers/render.py +++ b/korman/properties/modifiers/render.py @@ -34,7 +34,70 @@ class PlasmaDecalManagerRef(bpy.types.PropertyGroup): options=set()) -class PlasmaDecalReceiveMod(PlasmaModifierProperties): +class PlasmaDecalMod: + def _iter_decals(self, func): + for decal_ref in self.managers: + if decal_ref.enabled: + func(decal_ref.name) + + @classmethod + def register(cls): + cls.managers = CollectionProperty(type=PlasmaDecalManagerRef) + cls.active_manager_index = IntProperty(options={"HIDDEN"}) + + +class PlasmaDecalPrintMod(PlasmaDecalMod, PlasmaModifierProperties): + pl_id = "decal_print" + + bl_category = "Render" + bl_label = "Print Decal" + bl_description = "Prints a decal onto an object" + + decal_type = EnumProperty(name="Decal Type", + description="Type of decal to print onto another object", + items=[("DYNAMIC", "Dynamic", "This object prints a decal onto dynamic decal surfaces"), + ("STATIC", "Static", "This object is a decal itself")], + options=set()) + + # Dynamic Decals + length = FloatProperty(name="Length", + min=0.1, soft_max=30.0, precision=2, + default=0.45, + options=set()) + width = FloatProperty(name="Width", + min=0.1, soft_max=30.0, precision=2, + default=0.9, + options=set()) + height = FloatProperty(name="Height", + min=0.1, soft_max=30.0, precision=2, + default=1.0, + options=set()) + + @property + def copy_material(self): + return self.decal_type == "STATIC" + + def get_key(self, exporter, so): + if self.decal_type == "DYNAMIC": + pClass = plActivePrintShape if any((i.enabled for i in self.managers)) else plPrintShape + return exporter.mgr.find_create_key(pClass, so=so) + + def export(self, exporter, bo, so): + if self.decal_type == "STATIC": + exporter.decal.export_static_decal(bo) + elif self.decal_type == "DYNAMIC": + print_shape = self.get_key(exporter, so).object + print_shape.length = self.length + print_shape.width = self.width + print_shape.height = self.height + + def post_export(self, exporter, bo, so): + if self.decal_type == "DYNAMIC": + print_shape = self.get_key(exporter, so).object + f = functools.partial(exporter.decal.export_active_print_shape, print_shape) + self._iter_decals(f) + +class PlasmaDecalReceiveMod(PlasmaDecalMod, PlasmaModifierProperties): pl_id = "decal_receive" bl_category = "Render" @@ -44,11 +107,6 @@ class PlasmaDecalReceiveMod(PlasmaModifierProperties): managers = CollectionProperty(type=PlasmaDecalManagerRef) active_manager_index = IntProperty(options={"HIDDEN"}) - def _iter_decals(self, func): - for decal_ref in self.managers: - if decal_ref.enabled: - func(decal_ref.name) - def export(self, exporter, bo, so): f = functools.partial(exporter.decal.generate_dynamic_decal, bo) self._iter_decals(f) @@ -231,6 +289,10 @@ class PlasmaLightMapGen(idprops.IDPropMixin, PlasmaModifierProperties, PlasmaMod else: return self.bake_type == "lightmap" + @property + def copy_material(self): + return self.bake_lightmap + def export(self, exporter, bo, so): # If we're exporting vertex colors, who gives a rat's behind? if not self.bake_lightmap: diff --git a/korman/properties/modifiers/water.py b/korman/properties/modifiers/water.py index 9920e77..e97d554 100644 --- a/korman/properties/modifiers/water.py +++ b/korman/properties/modifiers/water.py @@ -238,6 +238,10 @@ class PlasmaWaterModifier(idprops.IDPropMixin, PlasmaModifierProperties, bpy.typ min=-10.0, max=10.0, default=0.0) + @property + def copy_material(self): + return True + def export(self, exporter, bo, so): waveset = exporter.mgr.find_create_object(plWaveSet7, name=bo.name, so=so) if self.wind_object: diff --git a/korman/properties/prop_scene.py b/korman/properties/prop_scene.py index 6d9dc76..61b66a2 100644 --- a/korman/properties/prop_scene.py +++ b/korman/properties/prop_scene.py @@ -15,6 +15,7 @@ import bpy from bpy.props import * +import itertools from ..exporter.etlight import _NUM_RENDER_LAYERS @@ -48,7 +49,8 @@ class PlasmaDecalManager(bpy.types.PropertyGroup): prev_value = self.name for i in bpy.data.objects: decal_receive = i.plasma_modifiers.decal_receive - for j in decal_receive.managers: + decal_print = i.plasma_modifiers.decal_print + for j in itertools.chain(decal_receive.managers, decal_print.managers): if j.name == prev_value: j.name = value self.name = value diff --git a/korman/ui/modifiers/render.py b/korman/ui/modifiers/render.py index 19afc84..fa4e5f6 100644 --- a/korman/ui/modifiers/render.py +++ b/korman/ui/modifiers/render.py @@ -27,6 +27,32 @@ class DecalMgrListUI(bpy.types.UIList): layout.label("[Empty]") +def decal_print(modifier, layout, context): + layout.prop(modifier, "decal_type") + + layout = layout.column() + layout.enabled = modifier.decal_type == "DYNAMIC" + layout.label("Dimensions:") + row = layout.row(align=True) + row.prop(modifier, "length") + row.prop(modifier, "width") + row.prop(modifier, "height") + layout.separator() + + ui_list.draw_modifier_list(layout, "DecalMgrListUI", modifier, "managers", + "active_manager_index", rows=2, maxrows=3) + try: + mgr_ref = modifier.managers[modifier.active_manager_index] + except: + pass + else: + scene = context.scene.plasma_scene + decal_mgr = next((i for i in scene.decal_managers if i.display_name == mgr_ref), None) + + layout.alert = decal_mgr is None + layout.prop_search(mgr_ref, "name", scene, "decal_managers", icon="NONE") + layout.alert = False + def decal_receive(modifier, layout, context): ui_list.draw_modifier_list(layout, "DecalMgrListUI", modifier, "managers", "active_manager_index", rows=2, maxrows=3) From 8fe53c1518c5eeea73103f5437dbeb5cf8d28b17 Mon Sep 17 00:00:00 2001 From: Adam Johnson Date: Fri, 20 Dec 2019 22:37:31 -0500 Subject: [PATCH 4/5] Implement wet footprints. --- korman/exporter/decal.py | 27 ++++++++++++++++++++++++--- korman/properties/prop_scene.py | 28 ++++++++++++++++++++++++++-- korman/ui/ui_scene.py | 30 +++++++++++++++++++++++++++++- 3 files changed, 79 insertions(+), 6 deletions(-) diff --git a/korman/exporter/decal.py b/korman/exporter/decal.py index 33461a1..d6120b2 100644 --- a/korman/exporter/decal.py +++ b/korman/exporter/decal.py @@ -15,6 +15,7 @@ import bpy from collections import defaultdict +import itertools from PyHSPlasma import * import weakref @@ -34,7 +35,8 @@ def _get_footprint_class(exporter, name, vs): class DecalConverter: _decal_lookup = { - "footprint": _get_footprint_class, + "footprint_dry": _get_footprint_class, + "footprint_wet": _get_footprint_class, "puddle": _get_puddle_class, "ripple": lambda e, name, vs: plDynaRippleVSMgr if vs else plDynaRippleMgr, } @@ -42,6 +44,7 @@ class DecalConverter: def __init__(self, exporter): self._decal_managers = defaultdict(list) self._exporter = weakref.ref(exporter) + self._notifies = defaultdict(set) def add_dynamic_decal_receiver(self, so, decal_name): # One decal manager in Blender can map to many Plasma decal managers. @@ -61,6 +64,15 @@ class DecalConverter: if key.location == so_loc and getattr(decal_mgr, "waveSet", None) == waveset: decal_mgr.addTarget(so_key) + # HACKAGE: Add the wet/dirty notifes now that we know about all the decal managers. + notify_names = self._notifies[decal_name] + notify_keys = itertools.chain.from_iterable((self._decal_managers[i] for i in notify_names)) + for notify_key in notify_keys: + for i in (i.object for i in decal_mgrs): + i.addNotify(notify_key) + # Don't need to do that again. + del self._notifies[decal_name] + def export_active_print_shape(self, print_shape, decal_name): decal_mgrs = self._decal_managers.get(decal_name) if decal_mgrs is None: @@ -106,7 +118,7 @@ class DecalConverter: self._decal_managers[decal_name].append(decal_mgr.key) # Certain decals are required to be squares - if decal_type in {"footprint", "wake"}: + if decal_type in {"footprint_dry", "footprint_wet", "wake"}: length, width = decal.length / 100.0, decal.width / 100.0 else: length = max(decal.length, decal.width) / 100.0 @@ -129,7 +141,7 @@ class DecalConverter: decal_mgr.scale = hsVector3(length, width, 1.0) # Hardwired calculations from PlasmaMAX - if decal_type in {"footprint", "bullet"}: + if decal_type in {"footprint_dry", "footprint_wet", "bullet"}: decal_mgr.rampEnd = 0.1 decal_mgr.decayStart = decal.life_span - (decal.life_span * 0.25) decal_mgr.lifeSpan = decal.life_span @@ -141,6 +153,15 @@ class DecalConverter: else: raise RuntimeError() + # While any decal manager can be wet/dry, it really makes the most sense to only + # expose wet footprints. In the future, we could expose the plDynaDecalEnableMsg + # to nodes for advanced hacking. + decal_mgr.waitOnEnable = decal_type == "footprint_wet" + if decal_type in {"puddle", "ripple"}: + decal_mgr.wetLength = decal.wet_time + self._notifies[decal_name].update((i.name for i in decal.wet_managers + if i.enabled and i.name != decal_name)) + # UV Animations are hardcoded in PlasmaMAX. Any reason why we should expose this? # I can't think of any presently... Note testing the final instance instead of the # artist setting in case that gets overridden (puddle -> ripple) diff --git a/korman/properties/prop_scene.py b/korman/properties/prop_scene.py index 61b66a2..81cb2c5 100644 --- a/korman/properties/prop_scene.py +++ b/korman/properties/prop_scene.py @@ -42,6 +42,16 @@ class PlasmaBakePass(bpy.types.PropertyGroup): default=((True,) * _NUM_RENDER_LAYERS)) +class PlasmaWetDecalRef(bpy.types.PropertyGroup): + enabled = BoolProperty(name="Enabled", + default=True, + options=set()) + + name = StringProperty(name="Decal Name", + description="Wet decal manager", + options=set()) + + class PlasmaDecalManager(bpy.types.PropertyGroup): def _get_display_name(self): return self.name @@ -53,6 +63,10 @@ class PlasmaDecalManager(bpy.types.PropertyGroup): for j in itertools.chain(decal_receive.managers, decal_print.managers): if j.name == prev_value: j.name = value + for i in bpy.context.scene.plasma_scene.decal_managers: + for j in i.wet_managers: + if j.name == prev_value: + j.name = value self.name = value name = StringProperty(name="Decal Name", @@ -64,10 +78,11 @@ class PlasmaDecalManager(bpy.types.PropertyGroup): decal_type = EnumProperty(name="Decal Type", description="", - items=[("footprint", "Footprint", ""), + items=[("footprint_dry", "Footprint (Dry)", ""), + ("footprint_wet", "Footprint (Wet)", ""), ("puddle", "Water Ripple (Shallow)", ""), ("ripple", "Water Ripple (Deep)", "")], - default="footprint", + default="footprint_dry", options=set()) image = PointerProperty(name="Image", description="", @@ -102,6 +117,15 @@ class PlasmaDecalManager(bpy.types.PropertyGroup): subtype="TIME", unit="TIME", min=0.0, soft_max=300.0, default=30.0, options=set()) + wet_time = FloatProperty(name="Wet Time", + description="How long the decal print shapes stay wet after losing contact with this surface", + subtype="TIME", unit="TIME", + min=0.0, soft_max=300.0, default=10.0, + options=set()) + + # Footprints to wet-ize + wet_managers = CollectionProperty(type=PlasmaWetDecalRef) + active_wet_index = IntProperty(options={"HIDDEN"}) class PlasmaScene(bpy.types.PropertyGroup): diff --git a/korman/ui/ui_scene.py b/korman/ui/ui_scene.py index efcac8e..dc2d4c0 100644 --- a/korman/ui/ui_scene.py +++ b/korman/ui/ui_scene.py @@ -32,6 +32,15 @@ class DecalManagerListUI(bpy.types.UIList): layout.prop(item, "display_name", emboss=False, text="") +class WetManagerListUI(bpy.types.UIList): + def draw_item(self, context, layout, data, item, icon, active_data, active_property, index=0, flt_flag=0): + if item.name: + layout.label(item.name) + layout.prop(item, "enabled", text="") + else: + layout.label("[Empty]") + + class PlasmaDecalManagersPanel(SceneButtonsPanel, bpy.types.Panel): bl_label = "Plasma Decal Managers" @@ -68,5 +77,24 @@ class PlasmaDecalManagersPanel(SceneButtonsPanel, bpy.types.Panel): col.label("Draw Settings:") col.prop(decal_mgr, "intensity") sub = col.row() - sub.active = decal_mgr.decal_type in {"footprint", "bullet", "torpedo"} + sub.active = decal_mgr.decal_type in {"footprint_dry", "footprint_wet", "bullet", "torpedo"} sub.prop(decal_mgr, "life_span") + sub = col.row() + sub.active = decal_mgr.decal_type in {"puddle", "ripple"} + sub.prop(decal_mgr, "wet_time") + + if decal_mgr.decal_type in {"puddle", "ripple"}: + box.separator() + box.label("Wet Footprints:") + ui_list.draw_list(box, "WetManagerListUI", "scene", decal_mgr, "wet_managers", + "active_wet_index", rows=2, maxrows=3) + try: + wet_ref = decal_mgr.wet_managers[decal_mgr.active_wet_index] + except: + pass + else: + wet_mgr = next((i for i in scene.decal_managers if i.name == wet_ref.name), None) + box.alert = getattr(wet_mgr, "decal_type", None) == "footprint_wet" + box.prop_search(wet_ref, "name", scene, "decal_managers", icon="NONE") + if wet_ref.name == decal_mgr.name: + box.label(text="Circular reference", icon="ERROR") From 1e3855790e6a8656081a387ca9da4de03a5cc84f Mon Sep 17 00:00:00 2001 From: Adam Johnson Date: Tue, 4 Feb 2020 11:51:58 -0500 Subject: [PATCH 5/5] Add icons for decals. --- korman/ui/modifiers/render.py | 6 +++--- korman/ui/ui_scene.py | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/korman/ui/modifiers/render.py b/korman/ui/modifiers/render.py index fa4e5f6..64d3c9b 100644 --- a/korman/ui/modifiers/render.py +++ b/korman/ui/modifiers/render.py @@ -21,7 +21,7 @@ from ...exporter.mesh import _VERTEX_COLOR_LAYERS 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: - layout.label(item.name) + layout.label(item.name, icon="BRUSH_DATA") layout.prop(item, "enabled", text="") else: layout.label("[Empty]") @@ -50,7 +50,7 @@ def decal_print(modifier, layout, context): decal_mgr = next((i for i in scene.decal_managers if i.display_name == mgr_ref), None) layout.alert = decal_mgr is None - layout.prop_search(mgr_ref, "name", scene, "decal_managers", icon="NONE") + layout.prop_search(mgr_ref, "name", scene, "decal_managers", icon="BRUSH_DATA") layout.alert = False def decal_receive(modifier, layout, context): @@ -65,7 +65,7 @@ def decal_receive(modifier, layout, context): decal_mgr = next((i for i in scene.decal_managers if i.display_name == mgr_ref), None) layout.alert = decal_mgr is None - layout.prop_search(mgr_ref, "name", scene, "decal_managers", icon="NONE") + layout.prop_search(mgr_ref, "name", scene, "decal_managers", icon="BRUSH_DATA") def fademod(modifier, layout, context): layout.prop(modifier, "fader_type") diff --git a/korman/ui/ui_scene.py b/korman/ui/ui_scene.py index dc2d4c0..ec72d82 100644 --- a/korman/ui/ui_scene.py +++ b/korman/ui/ui_scene.py @@ -29,13 +29,13 @@ class SceneButtonsPanel: class DecalManagerListUI(bpy.types.UIList): def draw_item(self, context, layout, data, item, icon, active_data, active_property, index=0, flt_flag=0): - layout.prop(item, "display_name", emboss=False, text="") + layout.prop(item, "display_name", emboss=False, text="", icon="BRUSH_DATA") class WetManagerListUI(bpy.types.UIList): def draw_item(self, context, layout, data, item, icon, active_data, active_property, index=0, flt_flag=0): if item.name: - layout.label(item.name) + layout.label(item.name, icon="BRUSH_DATA") layout.prop(item, "enabled", text="") else: layout.label("[Empty]")