Browse Source

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.
pull/172/head
Adam Johnson 5 years ago
parent
commit
e2134d16ad
Signed by: Hoikas
GPG Key ID: 0B6515D6FF6F271E
  1. 2
      korman/exporter/convert.py
  2. 141
      korman/exporter/decal.py
  3. 83
      korman/exporter/material.py
  4. 33
      korman/properties/modifiers/render.py
  5. 64
      korman/properties/prop_scene.py
  6. 1
      korman/ui/__init__.py
  7. 23
      korman/ui/modifiers/render.py
  8. 72
      korman/ui/ui_scene.py

2
korman/exporter/convert.py

@ -21,6 +21,7 @@ import time
from . import animation from . import animation
from . import camera from . import camera
from . import decal
from . import explosions from . import explosions
from . import etlight from . import etlight
from . import image from . import image
@ -54,6 +55,7 @@ class Exporter:
self.camera = camera.CameraConverter(self) self.camera = camera.CameraConverter(self)
self.image = image.ImageCache(self) self.image = image.ImageCache(self)
self.locman = locman.LocalizationConverter(self) self.locman = locman.LocalizationConverter(self)
self.decal = decal.DecalConverter(self)
# Step 0.8: Init the progress mgr # Step 0.8: Init the progress mgr
self.mesh.add_progress_presteps(self.report) self.mesh.add_progress_presteps(self.report)

141
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 <http://www.gnu.org/licenses/>.
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

83
korman/exporter/material.py

@ -90,6 +90,7 @@ class _Texture:
self.ephemeral = kwargs.get("ephemeral", False) self.ephemeral = kwargs.get("ephemeral", False)
self.image = image self.image = image
self.tag = kwargs.get("tag", None) self.tag = kwargs.get("tag", None)
self.name = kwargs.get("name", image.name)
def __eq__(self, other): def __eq__(self, other):
if not isinstance(other, _Texture): if not isinstance(other, _Texture):
@ -106,17 +107,17 @@ class _Texture:
def __str__(self): def __str__(self):
if self.extension is None: if self.extension is None:
name = self.image.name name = self.name
else: 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: if self.calc_alpha:
name = "ALPHAGEN_{}".format(name) name = "ALPHAGEN_{}".format(self.name)
if self.is_detail_map: if self.is_detail_map:
name = "DETAILGEN_{}-{}-{}-{}-{}_{}".format(self._DETAIL_BLEND[self.detail_blend], name = "DETAILGEN_{}-{}-{}-{}-{}_{}".format(self._DETAIL_BLEND[self.detail_blend],
self.detail_fade_start, self.detail_fade_stop, self.detail_fade_start, self.detail_fade_stop,
self.detail_opacity_start, self.detail_opacity_stop, self.detail_opacity_start, self.detail_opacity_stop,
name) self.name)
return name return name
def _update(self, other): def _update(self, other):
@ -260,6 +261,78 @@ class MaterialConverter:
# Looks like we're done... # Looks like we're done...
return hsgmat.key 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): def export_waveset_material(self, bo, bm):
self._report.msg("Exporting WaveSet Material '{}'", bm.name, indent=1) self._report.msg("Exporting WaveSet Material '{}'", bm.name, indent=1)
@ -818,7 +891,7 @@ class MaterialConverter:
image.pixels = pixels image.pixels = pixels
self.export_prepared_image(image=image, owner=owner, allowed_formats={"BMP"}, 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): def export_prepared_image(self, **kwargs):
"""This exports an externally prepared image and an optional owning layer. """This exports an externally prepared image and an optional owning layer.

33
korman/properties/modifiers/render.py

@ -16,6 +16,7 @@
import bpy import bpy
from bpy.props import * from bpy.props import *
import functools
from PyHSPlasma import * from PyHSPlasma import *
from .base import PlasmaModifierProperties, PlasmaModifierUpgradable from .base import PlasmaModifierProperties, PlasmaModifierUpgradable
@ -24,6 +25,38 @@ from ...exporter import utils
from ...exporter.explosions import ExportError from ...exporter.explosions import ExportError
from ... import idprops 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): class PlasmaFadeMod(PlasmaModifierProperties):
pl_id = "fademod" pl_id = "fademod"

64
korman/properties/prop_scene.py

@ -41,10 +41,74 @@ class PlasmaBakePass(bpy.types.PropertyGroup):
default=((True,) * _NUM_RENDER_LAYERS)) 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): class PlasmaScene(bpy.types.PropertyGroup):
bake_passes = CollectionProperty(type=PlasmaBakePass) bake_passes = CollectionProperty(type=PlasmaBakePass)
active_pass_index = IntProperty(options={"HIDDEN"}) 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", modifier_copy_object = PointerProperty(name="INTERNAL: Object to copy modifiers from",
options={"HIDDEN", "SKIP_SAVE"}, options={"HIDDEN", "SKIP_SAVE"},
type=bpy.types.Object) type=bpy.types.Object)

1
korman/ui/__init__.py

@ -21,6 +21,7 @@ from .ui_menus import *
from .ui_modifiers import * from .ui_modifiers import *
from .ui_object import * from .ui_object import *
from .ui_render_layer import * from .ui_render_layer import *
from .ui_scene import *
from .ui_text import * from .ui_text import *
from .ui_texture import * from .ui_texture import *
from .ui_toolbox import * from .ui_toolbox import *

23
korman/ui/modifiers/render.py

@ -18,6 +18,29 @@ import bpy
from .. import ui_list from .. import ui_list
from ...exporter.mesh import _VERTEX_COLOR_LAYERS 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): def fademod(modifier, layout, context):
layout.prop(modifier, "fader_type") layout.prop(modifier, "fader_type")

72
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 <http://www.gnu.org/licenses/>.
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")
Loading…
Cancel
Save