Skip to content

Commit 9178732

Browse files
HackLinjiuyuexushengj
authored andcommitted
add layer multiply and export refactor
1 parent b832202 commit 9178732

6 files changed

Lines changed: 222 additions & 16 deletions

File tree

src/preppipe/frontend/vnmodel/passes.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
from ...irbase import *
99
from .vncodegen import VNCodeGen
1010
from .vnparser import VNParser
11+
from .vnprune import prune_unused_asset_declarations
1112
from ...vnmodel import VNModel
1213

1314
@TransformArgumentGroup('vnparse', 'Options for VNModel source parsing')
@@ -55,3 +56,13 @@ def run(self) -> Operation | list[Operation] | None:
5556
ast = self.inputs[0]
5657
model = VNCodeGen.run(ast)
5758
return model
59+
60+
61+
@MiddleEndDecl('vn-prune-unused-decls', input_decl=VNModel, output_decl=VNModel)
62+
class VNPruneUnusedAssetsTransform(TransformBase):
63+
def run(self) -> Operation | list[Operation] | None:
64+
assert len(self.inputs) == 1
65+
model = self.inputs[0]
66+
if isinstance(model, VNModel):
67+
prune_unused_asset_declarations(model)
68+
return model
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
# SPDX-FileCopyrightText: 2023 PrepPipe's Contributors
2+
# SPDX-License-Identifier: Apache-2.0
3+
4+
from ...vnmodel import (
5+
VNModel,
6+
VNNamespace,
7+
VNCharacterSymbol,
8+
VNSceneSymbol,
9+
VNAssetValueSymbol,
10+
)
11+
12+
13+
def _prune_region(symbols):
14+
to_remove = [s for s in symbols if isinstance(s, VNAssetValueSymbol) and s.use_empty()]
15+
for s in to_remove:
16+
s.erase_from_parent()
17+
18+
19+
def prune_unused_asset_declarations(model: VNModel) -> None:
20+
"""原地移除 model 中未被使用的资源声明(依赖 Value 的 uselist)。"""
21+
for ns in model.namespace:
22+
if not isinstance(ns, VNNamespace):
23+
continue
24+
for c in ns.characters:
25+
if not isinstance(c, VNCharacterSymbol):
26+
continue
27+
_prune_region(c.sprites)
28+
_prune_region(c.sideimages)
29+
for scene in ns.scenes:
30+
if not isinstance(scene, VNSceneSymbol):
31+
continue
32+
_prune_region(scene.backgrounds)
33+
_prune_region(ns.assets)

src/preppipe/util/imagepack.py

Lines changed: 95 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,26 @@ def clear(self):
108108
self.layers.clear()
109109
self.min_cached_layer = 0
110110

111+
112+
class LayerBlendMode(enum.Enum):
113+
"""图层混合模式。NORMAL 为默认 alpha 合成,MULTIPLY 为正片叠底。
114+
序列化时使用 name.lower()(如 "normal", "multiply"),反序列化用 LayerBlendMode.from_serialized(str)。"""
115+
NORMAL = enum.auto()
116+
MULTIPLY = enum.auto()
117+
118+
def to_serialized(self) -> str:
119+
return self.name.lower()
120+
121+
@classmethod
122+
def from_serialized(cls, s : str | None) -> "LayerBlendMode":
123+
if s is None or s.strip() == "":
124+
return cls.NORMAL
125+
try:
126+
return cls[s.upper()]
127+
except KeyError:
128+
raise ValueError("Unsupported layer mode: " + s + " (only normal and multiply are supported)")
129+
130+
111131
@AssetClassDecl("imagepack")
112132
@ToolClassDecl("imagepack")
113133
class ImagePack(NamedAssetClassBase):
@@ -175,11 +195,13 @@ class LayerInfo:
175195
base : bool
176196
# 如果是 True 的话,该层可以不受限制地单独被选取
177197
toggle : bool
198+
# 图层混合模式
199+
mode : LayerBlendMode
178200

179201
def __init__(self, patch : ImageWrapper,
180202
offset_x : int = 0, offset_y : int = 0,
181203
width : int = 0, height : int = 0,
182-
base : bool = False, toggle : bool = False, basename : str = '') -> None:
204+
base : bool = False, toggle : bool = False, basename : str = '', mode : LayerBlendMode | None = None) -> None:
183205
self.patch = patch
184206
self.basename = basename
185207
self.offset_x = offset_x
@@ -188,6 +210,7 @@ def __init__(self, patch : ImageWrapper,
188210
self.height = height
189211
self.base = base
190212
self.toggle = toggle
213+
self.mode = mode if mode is not None else LayerBlendMode.NORMAL
191214
if width == 0 or height == 0:
192215
raise RuntimeError("Zero-sized layer?")
193216

@@ -199,7 +222,7 @@ def get_shrinked(self, ratio : decimal.Decimal):
199222
offset_x=int(self.offset_x * ratio),
200223
offset_y=int(self.offset_y * ratio),
201224
width=newwidth, height=newheight,
202-
base=self.base, toggle=self.toggle, basename=self.basename)
225+
base=self.base, toggle=self.toggle, basename=self.basename, mode=self.mode)
203226

204227
class CompositeInfo:
205228
# 保存时每个差分的信息
@@ -328,6 +351,8 @@ def collect_layer_group(prefix : str, layers : list[ImagePack.LayerInfo]):
328351
flags.append("toggle")
329352
if len(flags) > 0:
330353
jsonobj["flags"] = flags
354+
if l.mode != LayerBlendMode.NORMAL:
355+
jsonobj["mode"] = l.mode.to_serialized()
331356
result.append(jsonobj)
332357
self._write_image_to_path(l.patch, filename, path)
333358
return result
@@ -430,11 +455,16 @@ def read_layer_group(prefix, group_name):
430455
height = layer_info["h"]
431456
base = "base" in layer_info.get("flags", [])
432457
toggle = "toggle" in layer_info.get("flags", [])
458+
mode_str = layer_info.get("mode", None)
459+
try:
460+
mode = LayerBlendMode.from_serialized(mode_str)
461+
except ValueError as e:
462+
raise PPInternalError(str(e))
433463
layer_img = ImageWrapper(path=os.path.join(path, layer_filename))
434464
layers.append(ImagePack.LayerInfo(patch=layer_img,
435465
offset_x=offset_x, offset_y=offset_y,
436466
width=width, height=height,
437-
base=base, toggle=toggle, basename=layer_info["p"]))
467+
base=base, toggle=toggle, basename=layer_info["p"], mode=mode))
438468
return layers
439469

440470
self.layers = read_layer_group("l", "layers")
@@ -503,7 +533,11 @@ def get_composed_image_lower(self, layer_indices : list[int], composition_cache:
503533
for li in layer_indices:
504534
layer = self.layers[li]
505535
cur = result.crop((layer.offset_x, layer.offset_y, layer.offset_x + layer.width, layer.offset_y + layer.height))
506-
cur = PIL.Image.alpha_composite(cur, layer.patch.get())
536+
layer_patch = layer.patch.get()
537+
if layer.mode == LayerBlendMode.MULTIPLY:
538+
cur = ImagePack.apply_multiply_blend_mode(cur, layer_patch)
539+
else:
540+
cur = PIL.Image.alpha_composite(cur, layer_patch)
507541
if composition_cache is not None and li >= composition_cache.min_cached_layer:
508542
result = result.copy()
509543
result.paste(cur, (layer.offset_x, layer.offset_y))
@@ -513,6 +547,21 @@ def get_composed_image_lower(self, layer_indices : list[int], composition_cache:
513547
# print(f"get_composed_image_lower: {end-start} s")
514548
return ImageWrapper(image=result)
515549

550+
@staticmethod
551+
def apply_multiply_blend_mode(base : PIL.Image.Image, overlay : PIL.Image.Image) -> PIL.Image.Image:
552+
base_array = np.array(base, dtype=np.float32)
553+
overlay_array = np.array(overlay, dtype=np.float32)
554+
overlay_alpha = overlay_array[:, :, 3:4] / 255.0
555+
base_alpha = base_array[:, :, 3:4] / 255.0
556+
result_rgb = base_array[:, :, :3] * overlay_array[:, :, :3] / 255.0
557+
result_alpha = overlay_alpha + base_alpha * (1.0 - overlay_alpha)
558+
base_contribution = 1.0 - overlay_alpha
559+
final_rgb = result_rgb * overlay_alpha + base_array[:, :, :3] * base_contribution
560+
final_rgb = np.clip(final_rgb, 0, 255).astype(np.uint8)
561+
final_alpha = np.clip(result_alpha * 255, 0, 255).astype(np.uint8)
562+
result_array = np.dstack((final_rgb, final_alpha))
563+
return PIL.Image.fromarray(result_array, mode='RGBA')
564+
516565
@staticmethod
517566
def ndarray_hsv_to_rgb(hsv : np.ndarray) -> np.ndarray:
518567
#return np.apply_along_axis(ImagePack.hsv_to_rgb, 1, hsv)
@@ -726,7 +775,7 @@ def create_forked_layer(imgwidth : int, imgheight : int, l : LayerInfo, layerind
726775
return ImagePack.LayerInfo(ImageWrapper(image=newbase.crop(bbox)),
727776
offset_x=offset_x, offset_y=offset_y,
728777
width=xmax-offset_x, height=ymax-offset_y,
729-
base=True, toggle=l.toggle, basename=l.basename)
778+
base=True, toggle=l.toggle, basename=l.basename, mode=l.mode)
730779

731780
def fork_applying_mask(self, args : list[Color | PIL.Image.Image | str | tuple[str, Color] | None], enable_parallelization : bool = False):
732781
# 创建一个新的 imagepack, 将 mask 所影响的部分替换掉
@@ -1336,6 +1385,7 @@ def lookup_file(filename : str) -> str:
13361385
layer_dict[imgpathbase] = layerindex
13371386
flag_base = False
13381387
flag_toggle = False
1388+
flag_mode : LayerBlendMode = LayerBlendMode.NORMAL
13391389
def add_flag(flag : str):
13401390
nonlocal flag_base
13411391
nonlocal flag_toggle
@@ -1357,6 +1407,13 @@ def add_flag(flag : str):
13571407
elif isinstance(value, list):
13581408
for flag in value:
13591409
add_flag(flag)
1410+
elif key in ImagePack.TR_imagepack_yamlparse_mode.get_all_candidates():
1411+
if not isinstance(value, str):
1412+
raise PPInternalError("Invalid mode in " + yamlpath + ": expecting a str but got " + str(value))
1413+
if value in ImagePack.TR_imagepack_yamlparse_multiply.get_all_candidates():
1414+
flag_mode = LayerBlendMode.MULTIPLY
1415+
else:
1416+
raise PPInternalError("Unsupported layer mode: " + value + " (only normal and multiply are supported)")
13601417
imgpath = lookup_file(imgpathbase + ".png")
13611418
if not os.path.exists(os.path.join(basepath, imgpath)):
13621419
raise PPInternalError("Image file not found: " + imgpath)
@@ -1379,7 +1436,7 @@ def add_flag(flag : str):
13791436
newlayer = ImagePack.LayerInfo(patch,
13801437
offset_x=offset_x, offset_y=offset_y,
13811438
width=img.width, height=img.height,
1382-
base=flag_base, toggle=flag_toggle, basename=imgpathbase)
1439+
base=flag_base, toggle=flag_toggle, basename=imgpathbase, mode=flag_mode)
13831440
result.layers.append(newlayer)
13841441
if composites is None:
13851442
for i, layer in enumerate(result.layers):
@@ -1639,6 +1696,21 @@ class CharacterSpritePartsBased_PartKind(enum.Enum):
16391696
zh_cn="基底简写",
16401697
zh_hk="基底簡寫",
16411698
)
1699+
TR_imagepack_yamlgen_layer_modes = TR_imagepack.tr("layer_modes",
1700+
en="layer_modes",
1701+
zh_cn="图层模式",
1702+
zh_hk="圖層模式",
1703+
)
1704+
TR_imagepack_yamlparse_mode = TR_imagepack.tr("mode",
1705+
en="mode",
1706+
zh_cn="模式",
1707+
zh_hk="模式",
1708+
)
1709+
TR_imagepack_yamlparse_multiply = TR_imagepack.tr("multiply",
1710+
en="multiply",
1711+
zh_cn="正片叠底",
1712+
zh_hk="正片疊底",
1713+
)
16421714

16431715
@dataclasses.dataclass
16441716
class CharacterSpritePartsBased_PartsDecl:
@@ -1661,6 +1733,9 @@ def yaml_generation_charactersprite_parts_based(data : dict, layers : dict | Non
16611733
# 为了避免基底所用的名称太长(比如每个选区都有独立的图层、立绘有部分需要拆成独立的置顶的图层),我们支持定义基底组合的简称,如果有简称的话所有基底组合都必须有简称
16621734
base_abbreviation : dict[str, tuple[str,...]] | None = None
16631735

1736+
# 存储图层模式映射:图层名 -> 模式
1737+
layer_modes : dict[str, LayerBlendMode] = {}
1738+
16641739
kinds_enum_map : dict[str, ImagePack.CharacterSpritePartsBased_PartKind] = {}
16651740

16661741
# 除了装饰部件以外,每种部件类型最多只能声明一次
@@ -1785,11 +1860,22 @@ def add_parsed_info_to_metadata():
17851860
if not isinstance(abbr_list, str):
17861861
raise PPInternalError("Invalid abbreviation list in generation: expecting a str but got " + str(abbr_list) + " (type: " + str(type(abbr_list)) + ")")
17871862
base_abbreviation[abbr_name] = parse_dep_liststr(abbr_list)
1863+
elif k in ImagePack.TR_imagepack_yamlgen_layer_modes.get_all_candidates():
1864+
if not isinstance(v, dict):
1865+
raise PPInternalError("Invalid layer_modes in generation: expecting a dict but got " + str(v) + " (type: " + str(type(v)) + ")")
1866+
for layer_name, mode in v.items():
1867+
if not isinstance(mode, str):
1868+
raise PPInternalError("Invalid layer mode for " + layer_name + ": expecting a str but got " + str(mode) + " (type: " + str(type(mode)) + ")")
1869+
if mode in ImagePack.TR_imagepack_yamlparse_multiply.get_all_candidates():
1870+
layer_modes[layer_name] = LayerBlendMode.MULTIPLY
1871+
else:
1872+
raise PPInternalError("Unsupported layer mode for " + layer_name + ": " + mode + " (only normal and multiply are supported)")
17881873
else:
17891874
raise PPInternalError("Unknown key in generation: " + k + "(supported keys: "
17901875
+ str(ImagePack.TR_imagepack_yamlgen_parts.get_all_candidates()) + ", "
17911876
+ str(ImagePack.TR_imagepack_yamlgen_parts_kind.get_all_candidates()) + ", "
1792-
+ str(ImagePack.TR_imagepack_yamlgen_tags.get_all_candidates()) + ")")
1877+
+ str(ImagePack.TR_imagepack_yamlgen_tags.get_all_candidates()) + ", "
1878+
+ str(ImagePack.TR_imagepack_yamlgen_layer_modes.get_all_candidates()) + ")")
17931879
# 初步解析完毕,我们将解析结果写入 metadata
17941880
add_parsed_info_to_metadata()
17951881
# 检查一下输入有没有问题,所有标签是否都有定义,所有部件是否都有定义
@@ -1937,6 +2023,8 @@ def try_add_combinations(parts_list : tuple[str, ...]):
19372023
part_dict = {}
19382024
if kinds_enum_map[part.kind] == ImagePack.CharacterSpritePartsBased_PartKind.BASE:
19392025
part_dict[ImagePack.TR_imagepack_yamlparse_flags.get()] = ImagePack.TR_imagepack_yamlparse_base.get()
2026+
if part.name in layer_modes and layer_modes[part.name] == LayerBlendMode.MULTIPLY:
2027+
part_dict[ImagePack.TR_imagepack_yamlparse_mode.get()] = ImagePack.TR_imagepack_yamlparse_multiply.get()
19402028
layer_orders[part.name] = len(result_layers)
19412029
result_layers[part.name] = part_dict
19422030
result_composites : dict[str, list[str]] = {}

src/preppipe/util/imagepackexportop.py

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -241,7 +241,9 @@ def get_imagepack_layer_filename(self, pack_id : str, layer_index : int) -> str:
241241

242242
def place_imagepack_composite(self, instance_id : str, pack_id : str, composite_code : str) -> str:
243243
# 组合的图片应该放在哪
244-
return os.path.join("images", pack_id, 'E' + instance_id + '_' + composite_code + ".png")
244+
# 使用正斜杠以确保跨平台兼容性(RenPy 等引擎需要正斜杠)
245+
path = os.path.join("images", pack_id, 'E' + instance_id + '_' + composite_code + ".png")
246+
return path.replace("\\", "/")
245247

246248
# 以下是提供给子类使用者的接口
247249
# ---------------------------------------------------------------------
@@ -334,7 +336,21 @@ def _add_value_impl(self, value : ImagePackElementLiteralExpr, is_require_merged
334336
target_size_tuple = (value.size.value[0], value.size.value[1])
335337
composite_code = value.composite_name.value
336338
composite_index = instance_info.descriptor.get_composite_index_from_code(composite_code)
337-
if is_require_merged_image or target_size_tuple != instance_info.descriptor.size:
339+
# 检查该组合中是否有图层使用了非默认的混合模式
340+
# 如果有,我们需要使用预合成图片,因为 RenPy 的 layeredimage 不支持自定义混合模式
341+
has_non_default_blend_mode = False
342+
try:
343+
imagepack = AssetManager.get_instance().get_asset(pack_id)
344+
if isinstance(imagepack, ImagePack) and imagepack.is_imagedata_loaded():
345+
for layer_index in instance_info.descriptor.get_layers_from_composite_index(composite_index):
346+
layer = imagepack.layers[layer_index]
347+
if layer.mode != LayerBlendMode.NORMAL:
348+
has_non_default_blend_mode = True
349+
break
350+
except:
351+
# 如果无法访问 ImagePack 或图层信息不可用,保守地假设没有混合模式
352+
pass
353+
if is_require_merged_image or target_size_tuple != instance_info.descriptor.size or has_non_default_blend_mode:
338354
# 我们需要生成一个单独的图片
339355
key_tuple = (composite_index, target_size_tuple)
340356
if key_tuple in instance_info.composite_export_dict:

0 commit comments

Comments
 (0)