From 6bdab09a9e7eeac3f890225897e7b17c46efaf4b Mon Sep 17 00:00:00 2001 From: Lars Viklund Date: Fri, 5 Apr 2024 02:17:30 +0200 Subject: [PATCH 1/2] compose map icons like the game With the introduction of five new purple T17 maps in 3.24 the old approximation of how maps were composed from a base plate and a colorized sigil was insufficient. This change implements the relevant parts of the game UI shader for tinting UI elements like the map items, ignoring parts that are not used for these assets like saturation (always 1.0) and colour scaling (also always 1.0). For reference, see `PShad` in `Draw2D.hlsl`. The colour tint for the four different tier groups is obtained from the game renderer for maximum correctness. This also updates the defunct `atlas_icons` export to tint the sigils in the same way. --- PyPoE/cli/exporter/wiki/parsers/item.py | 86 ++++++++++++------- .../poe/file/specification/data/generated.py | 6 ++ 2 files changed, 59 insertions(+), 33 deletions(-) diff --git a/PyPoE/cli/exporter/wiki/parsers/item.py b/PyPoE/cli/exporter/wiki/parsers/item.py index 8c31752a..3f7b9fc7 100644 --- a/PyPoE/cli/exporter/wiki/parsers/item.py +++ b/PyPoE/cli/exporter/wiki/parsers/item.py @@ -84,6 +84,19 @@ def gemshade_constants_from_hex(hex_text: str): buf = codecs.decode(hex_text.replace(" ", ""), "hex") return GemShadeConstants(*struct.unpack("= 0.04045], + [lambda v: v / 12.92, lambda v: ((v + 0.055) / 1.055) ** 2.4], + ) + +def _linear_to_srgb(img): + return np.piecewise( + img, + [img < 0.0031308, img >= 0.0031308], + [lambda v: v * 12.92, lambda v: 1.055 * v ** (1.0 / 2.4) - 0.055], + ) def _apply_column_map(infobox, column_map: tuple[tuple[str, dict], ...], list_object): for k, data in column_map: @@ -450,14 +463,10 @@ class ItemsParser(SkillParserShared): ) _MAP_COLORS = { - "mid tier": "255,210,100", - "high tier": "240,30,10", - } - - # Midpoint values are the luminosities of _MAP_COLORS entries - _MAP_COLOR_MIDPOINTS = { - "mid tier": 211, - "high tier": 91, + "low tier": (248, 248, 248), + "mid tier": (252, 159, 14), + "high tier": (235, 3, 0), + "purple tier": (131, 54, 231), } _MAP_RELEASE_VERSION = { @@ -3902,20 +3911,6 @@ def _get_gem_icon_process(self, infobox: dict[str, str]): attr = max(attrs, key=attrs.get) var = infobox.pop("gem_shader") - def _srgb_to_linear(img): - return np.piecewise( - img, - [img < 0.04045, img >= 0.04045], - [lambda v: v / 12.92, lambda v: ((v + 0.055) / 1.055) ** 2.4], - ) - - def _linear_to_srgb(img): - return np.piecewise( - img, - [img < 0.0031308, img >= 0.0031308], - [lambda v: v * 12.92, lambda v: 1.055 * v ** (1.0 / 2.4) - 0.055], - ) - def process(img: Image): adorn = img.crop((0, 0, 78, 78)) base = img.crop((2 * 78, 0, 3 * 78, 78)) @@ -4056,6 +4051,7 @@ def export_map_icons(self, parsed_args): # output base icon (without map symbol) to .../Base.dds base_ico = os.path.join(self._img_path, "Base.dds") + purple_ico = os.path.join(self._img_path, "Tier17.dds") # read from the file path in the BaseIcon_DDSFile field from MapSeries.dat. self._write_dds( @@ -4064,6 +4060,13 @@ def export_map_icons(self, parsed_args): parsed_args=parsed_args, ) + # read from the file path in the Purple_DDSFile field from MapSeries.dat. + self._write_dds( + data=self.file_system.get_file(map_series["Purple_DDSFile"]), + out_path=purple_ico, + parsed_args=parsed_args, + ) + # === Maps from Atlas === for atlas_node in self.rr["AtlasNode.dat64"]: if not atlas_node["ItemVisualIdentityKey"]["DDSFile"]: @@ -4090,17 +4093,20 @@ def export_map_icons(self, parsed_args): if not os.path.isfile(ico_path): continue - # Tint with tier color, this historically differs from the colorization - # used for composing map glyphs onto on the itemized map base. img = Image.open(ico_path) - midpoint = self._MAP_COLOR_MIDPOINTS[name] - img = _colorize_rgba( - img, "black", "white", mid=f"rgb({color})", midpoint=midpoint - ) + img = self._shade_sigil(img, color) img.save(out_path) return r + def _shade_sigil(self, tex, color): + color = np.reshape(np.array(color + (255,)), (1, 1, 4)) / 255.0 + samples = np.asarray(tex, np.float32) / 255.0 + tex_colour = np.dstack((_srgb_to_linear(samples[:, :, :3]), samples[:, :, 3])) + final = color * tex_colour + final[:, :, :3] = _linear_to_srgb(final[:, :, :3]) + return Image.fromarray(np.uint8(final * 255.), 'RGBA') + def export_map(self, parsed_args): r = ExporterResult() @@ -4160,8 +4166,18 @@ def export_map(self, parsed_args): parsed_args=parsed_args, ) + purple_ico = os.path.join(self._img_path, "Map purple icon.dds") + + self._write_dds( + data=self.file_system.get_file(map_series["Purple_DDSFile"]), + out_path=purple_ico, + parsed_args=parsed_args, + ) + base_ico = base_ico.replace(".dds", ".png") base_img = Image.open(base_ico) + purple_ico = purple_ico.replace(".dds", ".png") + purple_img = Image.open(purple_ico) self.rr["MapSeriesTiers.dat64"].build_index("MapsKey") self.rr["MapPurchaseCosts.dat64"].build_index("Tier") @@ -4318,6 +4334,8 @@ def export_map(self, parsed_args): img = Image.open(ico) img.save(ico) + plate_img = purple_img if 16 < starting_tier else base_img + # Recolor the map icon if appropriate and layer the map icon with the base icon. if base_item_type["Id"] not in MAPS_TO_SKIP_COLORING: color = None @@ -4325,21 +4343,23 @@ def export_map(self, parsed_args): color = self._MAP_COLORS["mid tier"] if 10 < starting_tier: color = self._MAP_COLORS["high tier"] + if 16 < starting_tier: + color = self._MAP_COLORS["purple tier"] # This isn't quite how the game actually makes these map icons, # so it isn't ideal, but it works. if color: - img = _colorize_rgba(img, "black", f"rgb({color})") + img = self._shade_sigil(img, color) img.save(ico) if base_item_type["Id"] not in MAPS_TO_SKIP_COMPOSITING: - canvas = Image.new(base_img.mode, base_img.size, (0, 0, 0, 0)) + canvas = Image.new(plate_img.mode, plate_img.size, (0, 0, 0, 0)) paste_origin = ( - (base_img.size[0] - img.size[0]) // 2, - (base_img.size[1] - img.size[1]) // 2, + (plate_img.size[0] - img.size[0]) // 2, + (plate_img.size[1] - img.size[1]) // 2, ) canvas.paste(img, paste_origin) - Image.alpha_composite(base_img, canvas).save(ico) + Image.alpha_composite(plate_img, canvas).save(ico) return r diff --git a/PyPoE/poe/file/specification/data/generated.py b/PyPoE/poe/file/specification/data/generated.py index 19f6e3ed..d9cf370c 100644 --- a/PyPoE/poe/file/specification/data/generated.py +++ b/PyPoE/poe/file/specification/data/generated.py @@ -18523,6 +18523,12 @@ file_path=True, file_ext=".dds", ), + Field( + name="Purple_DDSFile", + type="ref|string", + file_path=True, + file_ext=".dds", + ), ), ), "MapSeriesTiers.dat": File( From 8ffcb36aac4141fa4b7668facbe67fe74be048f9 Mon Sep 17 00:00:00 2001 From: Lars Viklund Date: Fri, 5 Apr 2024 02:37:54 +0200 Subject: [PATCH 2/2] format code with black --- PyPoE/cli/exporter/wiki/parsers/item.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/PyPoE/cli/exporter/wiki/parsers/item.py b/PyPoE/cli/exporter/wiki/parsers/item.py index 3f7b9fc7..ffcf53f2 100644 --- a/PyPoE/cli/exporter/wiki/parsers/item.py +++ b/PyPoE/cli/exporter/wiki/parsers/item.py @@ -84,6 +84,7 @@ def gemshade_constants_from_hex(hex_text: str): buf = codecs.decode(hex_text.replace(" ", ""), "hex") return GemShadeConstants(*struct.unpack("