Browse Source

Fix non-determinism in Python material attributes.

The dumb string lookup probably worked most of the time, but with recent
changes that can cause layers and materials to be renamed to things not
matching the pattern exactly, it's better to explicitly lookup the keys.
This will prevent Dynamic Text Maps from seemingly "breaking" for no
reason just because the lighting strategy changes.
pull/277/head
Adam Johnson 3 years ago
parent
commit
809581ed60
Signed by: Hoikas
GPG Key ID: 0B6515D6FF6F271E
  1. 66
      korman/exporter/material.py
  2. 84
      korman/nodes/node_python.py
  3. 9
      korman/properties/prop_texture.py
  4. 19
      korman/ui/ui_texture.py

66
korman/exporter/material.py

@ -14,14 +14,18 @@
# along with Korman. If not, see <http://www.gnu.org/licenses/>.
import bpy
import mathutils
from collections import defaultdict
import functools
import itertools
import math
import mathutils
from pathlib import Path
from PyHSPlasma import *
from typing import Sequence, Union
from typing import Iterator, Optional, Union
import weakref
from PyHSPlasma import *
from .explosions import *
from .. import helpers
from ..korlib import *
@ -133,7 +137,8 @@ class _Texture:
class MaterialConverter:
def __init__(self, exporter):
self._obj2mat = {}
self._obj2mat = defaultdict(dict)
self._obj2layer = defaultdict(lambda: defaultdict(lambda: defaultdict(list)))
self._bump_mats = {}
self._exporter = weakref.ref(exporter)
self._pending = {}
@ -258,13 +263,13 @@ class MaterialConverter:
# material had no Textures, we will need to initialize a default layer
if not hsgmat.layers:
layer = self._mgr.find_create_object(plLayer, name="{}_AutoLayer".format(mat_name), bl=bo)
self._obj2layer[bo][bm][None].append(layer.key)
self._propagate_material_settings(bo, bm, layer)
layer = self._export_layer_animations(bo, bm, None, 0, layer)
hsgmat.addLayer(layer.key)
# Cache this material for later
mat_list = self._obj2mat.setdefault(bo, [])
mat_list.append(hsgmat.key)
self._obj2mat[bo][bm] = hsgmat.key
# Looks like we're done...
return hsgmat.key
@ -496,6 +501,10 @@ class MaterialConverter:
# NOTE: animated stencils and bumpmaps are nonsense.
if not slot.use_stencil and not wantBumpmap:
layer = self._export_layer_animations(bo, bm, slot, idx, layer)
# Stash the top of the stack for later in the export
if bm is not None:
self._obj2layer[bo][bm][texture].append(layer.key)
return layer
def _export_layer_animations(self, bo, bm, tex_slot, idx, base_layer) -> plLayer:
@ -791,8 +800,8 @@ class MaterialConverter:
if texture.image is None:
dtm = self._mgr.find_create_object(plDynamicTextMap, name="{}_DynText".format(layer.key.name), bl=bo)
dtm.hasAlpha = texture.use_alpha
# if you have a better idea, let's hear it...
dtm.visWidth, dtm.visHeight = 1024, 1024
dtm.visWidth = int(layer_props.dynatext_resolution)
dtm.visHeight = int(layer_props.dynatext_resolution)
layer.texture = dtm.key
else:
detail_blend = TEX_DETAIL_ALPHA
@ -1183,8 +1192,45 @@ class MaterialConverter:
data[i] = level_data
return numLevels, eWidth, eHeight, [data,]
def get_materials(self, bo) -> Sequence[plKey]:
return self._obj2mat.get(bo, [])
def get_materials(self, bo: bpy.types.Object, bm: Optional[bpy.types.Material] = None) -> Iterator[plKey]:
material_dict = self._obj2mat.get(bo, {})
if bm is None:
return material_dict.values()
else:
return material_dict.get(bm, [])
def get_layers(self, bo: Optional[bpy.types.Object] = None,
bm: Optional[bpy.types.Material] = None,
tex: Optional[bpy.types.Texture] = None) -> Iterator[plKey]:
# All three? Simple.
if bo is not None and bm is not None and tex is not None:
yield from filter(None, self._obj2layer[bo][bm][tex])
return
if bo is None and bm is None and tex is None:
self._exporter().report.warn("Asking for all the layers we've ever exported, eh? You like living dangerously.", indent=2)
# What we want to do is filter _obj2layers:
# bo if set, or all objects
# bm if set, or tex.users_materials if set, or all materials... ON THE OBJECT(s)
# tex if set, or all layers... ON THE OBJECT(s) MATERIAL(s)
object_iter = lambda: (bo,) if bo is not None else self._obj2layer.keys()
if bm is not None:
material_seq = (bm,)
elif tex is not None:
material_seq = tex.users_material
else:
_iter = filter(lambda x: x and x.material, itertools.chain.from_iterable((i.material_slots for i in object_iter())))
# Performance turd: this could, in the worst case, block on creating a list of every material
# attached to every single Plasma Object in the current scene. This whole algorithm sucks,
# though, so whatever.
material_seq = [slot.material for slot in _iter]
for filtered_obj in object_iter():
for filtered_mat in material_seq:
all_texes = self._obj2layer[filtered_obj][filtered_mat]
filtered_texes = all_texes[tex] if tex is not None else itertools.chain.from_iterable(all_texes.values())
yield from filter(None, filtered_texes)
def get_base_layer(self, hsgmat):
try:

84
korman/nodes/node_python.py

@ -15,6 +15,8 @@
import bpy
from bpy.props import *
from collections.abc import Iterable
from contextlib import contextmanager
from pathlib import Path
from PyHSPlasma import *
@ -283,7 +285,7 @@ class PlasmaPythonFileNode(PlasmaVersionedNode, bpy.types.Node):
from_node = socket.links[0].from_node
value = from_node.value if socket.is_simple_value else from_node.get_key(exporter, so)
if not isinstance(value, (tuple, list)):
if isinstance(value, str) or not isinstance(value, Iterable):
value = (value,)
for i in value:
param = plPythonParameter()
@ -808,8 +810,16 @@ class PlasmaAttribTextureNode(idprops.IDPropMixin, PlasmaAttribNodeBase, bpy.typ
pl_attrib = ("ptAttribMaterial", "ptAttribMaterialList",
"ptAttribDynamicMap", "ptAttribMaterialAnimation")
def _poll_texture(self, value):
if self.material is not None:
def _poll_material(self, value: bpy.types.Material) -> bool:
# Don't filter materials by texture - this would (potentially) result in surprising UX
# in that you would have to clear the texture selection before being able to select
# certain materials.
if self.target_object is not None:
object_materials = (slot.material for slot in self.target_object.material_slots if slot and slot.material)
return value in object_materials
return True
def _poll_texture(self, value: bpy.types.Texture) -> bool:
# is this the type of dealio that we're looking for?
attrib = self.to_socket
if attrib is not None:
@ -821,13 +831,26 @@ class PlasmaAttribTextureNode(idprops.IDPropMixin, PlasmaAttribNodeBase, bpy.typ
if not self._is_animated(self.material, value):
return False
# must be a legal option... but is it a member of this material?
# must be a legal option... but is it a member of this material... or, if no material,
# any of the materials attached to the object?
if self.material is not None:
return value.name in self.material.texture_slots
elif self.target_object is not None:
for i in (slot.material for slot in self.target_object.material_slots if slot and slot.material):
if value in (slot.texture for slot in i.texture_slots if slot and slot.texture):
return True
return False
else:
return True
target_object = PointerProperty(name="Object",
description="",
type=bpy.types.Object,
poll=idprops.poll_drawable_objects)
material = PointerProperty(name="Material",
description="Material the texture is attached to",
type=bpy.types.Material)
type=bpy.types.Material,
poll=_poll_material)
texture = PointerProperty(name="Texture",
description="Texture to expose to Python",
type=bpy.types.Texture,
@ -839,35 +862,52 @@ class PlasmaAttribTextureNode(idprops.IDPropMixin, PlasmaAttribNodeBase, bpy.typ
self.outputs[0].link_limit = 1
def draw_buttons(self, context, layout):
layout.prop(self, "material")
if self.target_object is not None:
iter_materials = lambda: (i.material for i in self.target_object.material_slots if i and i.material)
if self.material is not None:
if self.material not in iter_materials():
layout.label("The selected material is not linked to the target object.", icon="ERROR")
layout.alert = True
if self.texture is not None:
if not frozenset(self.texture.users_material) & frozenset(iter_materials()):
layout.label("The selected texture is not on a material linked to the target object.", icon="ERROR")
layout.alert = True
layout.prop(self, "target_object")
layout.prop(self, "material")
layout.prop(self, "texture")
def get_key(self, exporter, so):
if self.material is None:
self.raise_error("Material must be specified")
if self.texture is None:
self.raise_error("Texture must be specified")
if not any((self.target_object, self.material, self.texture)):
self.raise_error("At least one of: target object, material, or texture must be specified.")
attrib = self.to_socket
if attrib is None:
self.raise_error("must be connected to a Python File node!")
attrib = attrib.attribute_type
material = self.material
texture = self.texture
# Your attribute stuff here...
layer_generator = exporter.mesh.material.get_layers(self.target_object, self.material, self.texture)
bottom_layers = (i.object.bottomOfStack for i in layer_generator)
if attrib == "ptAttribDynamicMap":
if not self._is_dyntext(texture):
self.raise_error("Texture '{}' is not a Dynamic Text Map".format(self.texture_name))
name = "{}_{}_DynText".format(material.name, texture.name)
return exporter.mgr.find_create_key(plDynamicTextMap, name=name, so=so)
elif self._is_animated(material, texture):
name = "{}_{}_LayerAnim".format(material_name, texture.name)
return exporter.mgr.find_create_key(plLayerAnimation, name=name, so=so)
yield from filter(lambda x: x and isinstance(x.object, plDynamicTextMap),
(i.object.texture for i in layer_generator))
elif attrib == "ptAttribMaterialAnimation":
yield from filter(lambda x: x and isinstance(x.object, plLayerAnimationBase), layer_generator)
elif attrib == "ptAttribMaterialList":
yield from filter(lambda x: x and not isinstance(x.object, plLayerAnimationBase), bottom_layers)
elif attrib == "ptAttribMaterial":
# Only return the first key; warn about others.
result_gen = filter(lambda x: x and not isinstance(x.object, plLayerAnimationBase), bottom_layers)
result = next(result_gen, None)
remainder = sum((1 for i in result))
if remainder > 1:
exporter.report.warn("'{}.{}': Expected a single layer, but mapped to {}. Make the settings more specific.",
self.id_data.name, self.path_from_id(), remainder + 1, indent=2)
if result is not None:
yield result
else:
name = "{}_{}".format(material.name, texture.name)
return exporter.mgr.find_create_key(plLayer, name=name, so=so)
raise RuntimeError(attrib)
@classmethod
def _idprop_mapping(cls):

9
korman/properties/prop_texture.py

@ -99,3 +99,12 @@ class PlasmaLayer(bpy.types.PropertyGroup):
description="Don't save the depth information, allowing rendering of layers behind this one",
default=False,
options=set())
dynatext_resolution = EnumProperty(name="Dynamic Text Map Resolution",
description="Size of the Dynamic Text Map's underlying image",
items=[("128", "128x128", ""),
("256", "256x256", ""),
("512", "512x512", ""),
("1024", "1024x1024", "")],
default="1024",
options=set())

19
korman/ui/ui_texture.py

@ -70,13 +70,20 @@ class PlasmaLayerPanel(TextureButtonsPanel, bpy.types.Panel):
split = layout.split()
col = split.column()
col.label("Animation:")
col.active = self._has_animation_data(context) and not use_stencil
col.prop(layer_props, "anim_auto_start")
col.prop(layer_props, "anim_loop")
sub = col.column()
sub.label("Animation:")
sub.active = self._has_animation_data(context) and not use_stencil
sub.prop(layer_props, "anim_auto_start")
sub.prop(layer_props, "anim_loop")
sub.separator()
sub.label("SDL Animation:")
sub.prop(layer_props, "anim_sdl_var", text="")
# Yes, two separator.
col.separator()
col.label("SDL Animation:")
col.prop(layer_props, "anim_sdl_var", text="")
col.separator()
sub = col.column()
sub.active = texture.type == "IMAGE" and texture.image is None
sub.prop_menu_enum(layer_props, "dynatext_resolution", text="Dynamic Text Size")
col = split.column()
col.label("Miscellaneous:")

Loading…
Cancel
Save