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..d6120b2 --- /dev/null +++ b/korman/exporter/decal.py @@ -0,0 +1,185 @@ +# 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 +import itertools +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_dry": _get_footprint_class, + "footprint_wet": _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) + 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. + # 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) + + # 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: + 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: + 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_dry", "footprint_wet", "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_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 + 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() + + # 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) + 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..b56ea8b 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): @@ -179,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) @@ -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) @@ -356,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: @@ -380,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 @@ -715,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) @@ -818,7 +896,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. @@ -1073,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) @@ -1132,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/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..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 @@ -37,19 +42,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 befec0d..856840c 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,96 @@ 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 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" + bl_label = "Receive Decal" + bl_description = "Allows this object to receive dynamic decals" + + managers = CollectionProperty(type=PlasmaDecalManagerRef) + active_manager_index = IntProperty(options={"HIDDEN"}) + + 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" @@ -81,10 +172,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" @@ -202,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 36d0675..81cb2c5 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 @@ -41,10 +42,99 @@ 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 + def _set_display_name(self, value): + prev_value = self.name + for i in bpy.data.objects: + decal_receive = i.plasma_modifiers.decal_receive + 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 + 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", + 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_dry", "Footprint (Dry)", ""), + ("footprint_wet", "Footprint (Wet)", ""), + ("puddle", "Water Ripple (Shallow)", ""), + ("ripple", "Water Ripple (Deep)", "")], + default="footprint_dry", + 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()) + 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): 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..64d3c9b 100644 --- a/korman/ui/modifiers/render.py +++ b/korman/ui/modifiers/render.py @@ -18,6 +18,55 @@ 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, icon="BRUSH_DATA") + layout.prop(item, "enabled", text="") + else: + 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="BRUSH_DATA") + 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) + 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="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 new file mode 100644 index 0000000..ec72d82 --- /dev/null +++ b/korman/ui/ui_scene.py @@ -0,0 +1,100 @@ +# 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="", 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, icon="BRUSH_DATA") + layout.prop(item, "enabled", text="") + else: + layout.label("[Empty]") + + +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_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")