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. 108
      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/>. # along with Korman. If not, see <http://www.gnu.org/licenses/>.
import bpy import bpy
import mathutils
from collections import defaultdict
import functools import functools
import itertools
import math import math
import mathutils
from pathlib import Path from pathlib import Path
from PyHSPlasma import * from typing import Iterator, Optional, Union
from typing import Sequence, Union
import weakref import weakref
from PyHSPlasma import *
from .explosions import * from .explosions import *
from .. import helpers from .. import helpers
from ..korlib import * from ..korlib import *
@ -133,7 +137,8 @@ class _Texture:
class MaterialConverter: class MaterialConverter:
def __init__(self, exporter): def __init__(self, exporter):
self._obj2mat = {} self._obj2mat = defaultdict(dict)
self._obj2layer = defaultdict(lambda: defaultdict(lambda: defaultdict(list)))
self._bump_mats = {} self._bump_mats = {}
self._exporter = weakref.ref(exporter) self._exporter = weakref.ref(exporter)
self._pending = {} self._pending = {}
@ -258,13 +263,13 @@ class MaterialConverter:
# material had no Textures, we will need to initialize a default layer # material had no Textures, we will need to initialize a default layer
if not hsgmat.layers: if not hsgmat.layers:
layer = self._mgr.find_create_object(plLayer, name="{}_AutoLayer".format(mat_name), bl=bo) 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) self._propagate_material_settings(bo, bm, layer)
layer = self._export_layer_animations(bo, bm, None, 0, layer) layer = self._export_layer_animations(bo, bm, None, 0, layer)
hsgmat.addLayer(layer.key) hsgmat.addLayer(layer.key)
# Cache this material for later # Cache this material for later
mat_list = self._obj2mat.setdefault(bo, []) self._obj2mat[bo][bm] = hsgmat.key
mat_list.append(hsgmat.key)
# Looks like we're done... # Looks like we're done...
return hsgmat.key return hsgmat.key
@ -496,6 +501,10 @@ class MaterialConverter:
# NOTE: animated stencils and bumpmaps are nonsense. # NOTE: animated stencils and bumpmaps are nonsense.
if not slot.use_stencil and not wantBumpmap: if not slot.use_stencil and not wantBumpmap:
layer = self._export_layer_animations(bo, bm, slot, idx, layer) 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 return layer
def _export_layer_animations(self, bo, bm, tex_slot, idx, base_layer) -> plLayer: def _export_layer_animations(self, bo, bm, tex_slot, idx, base_layer) -> plLayer:
@ -791,8 +800,8 @@ class MaterialConverter:
if texture.image is None: if texture.image is None:
dtm = self._mgr.find_create_object(plDynamicTextMap, name="{}_DynText".format(layer.key.name), bl=bo) dtm = self._mgr.find_create_object(plDynamicTextMap, name="{}_DynText".format(layer.key.name), bl=bo)
dtm.hasAlpha = texture.use_alpha dtm.hasAlpha = texture.use_alpha
# if you have a better idea, let's hear it... dtm.visWidth = int(layer_props.dynatext_resolution)
dtm.visWidth, dtm.visHeight = 1024, 1024 dtm.visHeight = int(layer_props.dynatext_resolution)
layer.texture = dtm.key layer.texture = dtm.key
else: else:
detail_blend = TEX_DETAIL_ALPHA detail_blend = TEX_DETAIL_ALPHA
@ -1183,8 +1192,45 @@ class MaterialConverter:
data[i] = level_data data[i] = level_data
return numLevels, eWidth, eHeight, [data,] return numLevels, eWidth, eHeight, [data,]
def get_materials(self, bo) -> Sequence[plKey]: def get_materials(self, bo: bpy.types.Object, bm: Optional[bpy.types.Material] = None) -> Iterator[plKey]:
return self._obj2mat.get(bo, []) 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): def get_base_layer(self, hsgmat):
try: try:

108
korman/nodes/node_python.py

@ -15,6 +15,8 @@
import bpy import bpy
from bpy.props import * from bpy.props import *
from collections.abc import Iterable
from contextlib import contextmanager from contextlib import contextmanager
from pathlib import Path from pathlib import Path
from PyHSPlasma import * from PyHSPlasma import *
@ -283,7 +285,7 @@ class PlasmaPythonFileNode(PlasmaVersionedNode, bpy.types.Node):
from_node = socket.links[0].from_node from_node = socket.links[0].from_node
value = from_node.value if socket.is_simple_value else from_node.get_key(exporter, so) 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,) value = (value,)
for i in value: for i in value:
param = plPythonParameter() param = plPythonParameter()
@ -808,26 +810,47 @@ class PlasmaAttribTextureNode(idprops.IDPropMixin, PlasmaAttribNodeBase, bpy.typ
pl_attrib = ("ptAttribMaterial", "ptAttribMaterialList", pl_attrib = ("ptAttribMaterial", "ptAttribMaterialList",
"ptAttribDynamicMap", "ptAttribMaterialAnimation") "ptAttribDynamicMap", "ptAttribMaterialAnimation")
def _poll_texture(self, value): 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:
attrib = attrib.attribute_type
if attrib == "ptAttribDynamicMap":
if not self._is_dyntext(value):
return False
elif attrib == "ptAttribMaterialAnimation":
if not self._is_animated(self.material, value):
return False
# 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: if self.material is not None:
# is this the type of dealio that we're looking for?
attrib = self.to_socket
if attrib is not None:
attrib = attrib.attribute_type
if attrib == "ptAttribDynamicMap":
if not self._is_dyntext(value):
return False
elif attrib == "ptAttribMaterialAnimation":
if not self._is_animated(self.material, value):
return False
# must be a legal option... but is it a member of this material?
return value.name in self.material.texture_slots return value.name in self.material.texture_slots
return False 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", material = PointerProperty(name="Material",
description="Material the texture is attached to", description="Material the texture is attached to",
type=bpy.types.Material) type=bpy.types.Material,
poll=_poll_material)
texture = PointerProperty(name="Texture", texture = PointerProperty(name="Texture",
description="Texture to expose to Python", description="Texture to expose to Python",
type=bpy.types.Texture, type=bpy.types.Texture,
@ -839,35 +862,52 @@ class PlasmaAttribTextureNode(idprops.IDPropMixin, PlasmaAttribNodeBase, bpy.typ
self.outputs[0].link_limit = 1 self.outputs[0].link_limit = 1
def draw_buttons(self, context, layout): def draw_buttons(self, context, layout):
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, "material")
if self.material is not None: layout.prop(self, "texture")
layout.prop(self, "texture")
def get_key(self, exporter, so): def get_key(self, exporter, so):
if self.material is None: if not any((self.target_object, self.material, self.texture)):
self.raise_error("Material must be specified") self.raise_error("At least one of: target object, material, or texture must be specified.")
if self.texture is None:
self.raise_error("Texture must be specified")
attrib = self.to_socket attrib = self.to_socket
if attrib is None: if attrib is None:
self.raise_error("must be connected to a Python File node!") self.raise_error("must be connected to a Python File node!")
attrib = attrib.attribute_type 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 attrib == "ptAttribDynamicMap":
if not self._is_dyntext(texture): yield from filter(lambda x: x and isinstance(x.object, plDynamicTextMap),
self.raise_error("Texture '{}' is not a Dynamic Text Map".format(self.texture_name)) (i.object.texture for i in layer_generator))
name = "{}_{}_DynText".format(material.name, texture.name) elif attrib == "ptAttribMaterialAnimation":
return exporter.mgr.find_create_key(plDynamicTextMap, name=name, so=so) yield from filter(lambda x: x and isinstance(x.object, plLayerAnimationBase), layer_generator)
elif self._is_animated(material, texture): elif attrib == "ptAttribMaterialList":
name = "{}_{}_LayerAnim".format(material_name, texture.name) yield from filter(lambda x: x and not isinstance(x.object, plLayerAnimationBase), bottom_layers)
return exporter.mgr.find_create_key(plLayerAnimation, name=name, so=so) 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: else:
name = "{}_{}".format(material.name, texture.name) raise RuntimeError(attrib)
return exporter.mgr.find_create_key(plLayer, name=name, so=so)
@classmethod @classmethod
def _idprop_mapping(cls): 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", description="Don't save the depth information, allowing rendering of layers behind this one",
default=False, default=False,
options=set()) 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() split = layout.split()
col = split.column() col = split.column()
col.label("Animation:") sub = col.column()
col.active = self._has_animation_data(context) and not use_stencil sub.label("Animation:")
col.prop(layer_props, "anim_auto_start") sub.active = self._has_animation_data(context) and not use_stencil
col.prop(layer_props, "anim_loop") 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.separator()
col.label("SDL Animation:") col.separator()
col.prop(layer_props, "anim_sdl_var", text="") 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 = split.column()
col.label("Miscellaneous:") col.label("Miscellaneous:")

Loading…
Cancel
Save