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