
今天的内容是柚子社作品的CG合成![[打call]](https://i0.hdslb.com/bfs/emote/431432c43da3ee5aab5b0e4f8931953e649e9975.png)
实际上,相较于前几刊,柚子社的CG合成可谓不费吹灰之力!
本文的创新点是,通过imagediffmap.csv避免枚举所有cg合成组合,并复刻游戏原有顺序和名称,而非使用AI对齐工具、或是使用合成程序穷举。
按照惯例,下文会分成坐标解析,发现组合规则和实操三部分!
与其它引擎不同,krkr引擎往往将每一组图片及其坐标封装在同一个pimg里。无论是使用krkrdump还是用Garbro2,都可以轻松的获取,在此不赘述,站内已有大量攻略。不过,本文使用的是krkrdump,并且推荐使用krkrdump,可以免去找imagediffmap.csv的麻烦。
无论如何,如图准备好您要的pimg即可。

然后使用FreeMote里的Psbdecompile.exe

为清晰展示,我将程序和一个pimg移入了新的文件夹。

PsbDecompile的用法是:框选pimg并拖动到PsbDecompile上。然后,就会得到一个同名的文件夹和同名json文件和resx.json

.resx.json与我们无关,可以忽略。用记事本或vs打开ev101a.json可以看到:

而这是先前PsbDescompile的输出:

对比发现,json文件首先给图片以代号,然后指定了每个图层的偏移和属性。 这正是我们合成时需要的图层偏移量。以上的内容前人已有充分的探讨,如有所忽略还请海涵。
前文只是知道了每个图片的内部代号和自带的位置信息,但我们仍不知道引擎如何判定一张图片母图还是底图,以及选择哪两张母图、底图。
重新翻阅后发现,这一关键信息位于imagediffmap.csv。此文件就在krkrdump的输出目录当中,如果没有在输出目录中看见,可能是由于krkrdump的配置文件中写了过滤器,推荐去github重下。默认应当是不过滤的。

仔细辨认后发现,文件中有两种语法,diff和seton。不过,在我截的图范围之外,diff几乎没有出现,推测为引擎使用的某种特殊语法。跳过diff语法不会影响我们的运行效果,因此重点讨论seton。以以下两句为例:
ev111ba,ev111a,seton,Ba 含义是:目标名称为ev111ba,目标属于ev111a系列,将Ba(json中赋予的代号)作为底图,完成。——也就是说,原封不动Ba。
ev111bb,ev111a,seton,Bb:Ba 含义是:目标名称为ev111bb,目标属于ev111a系列,将Ba(json中赋予的代号)作为底图,将Bb叠加其上。
综上,就是krkr引擎渲染的指定逻辑。
至此,已经有了合成所需要的所有信息。之后的合成如前几期,仍是底图-叠层的模式。可以开写了!
写程序的时候为保证方便性,前文“pimg拖动到PsbDecompile.exe”后不用做任何操作,将imagediffmap.csv放入该文件夹,程序也在此执行。我加入了多任务处理,合成速度在多核设备会明显提升。
import os
import re
import json
from PIL import Image
EXPORT_DIR = "export"
def draw_layer(canvas, img_dir, layer):
if not layer.get("visible", 1):
return
png = os.path.join(img_dir, f"{layer['layer_id']}.png")
if not os.path.exists(png):
return
img = Image.open(png).convert("RGBA")
opacity = layer.get("opacity", 255)
if opacity < 255:
alpha = img.split()[-1].point(lambda p: p * opacity // 255)
img.putalpha(alpha)
canvas.alpha_composite(img, (layer["left"], layer["top"]))
def find_layer(layers, name):
for l in layers:
if l.get("name") == name:
return l
raise KeyError(f"Layer not found: {name}")
def process_rule(line):
line = line.strip()
if not line or line.startswith("#"):
return
out_name, base_name, mode, rule = [x.strip() for x in line.split(",")]
if mode != "seton" and mode != "diff":
print(f"[SKIP] unsupported mode: {mode}")
return
json_file = f"{base_name}.json"
img_dir = base_name
if not os.path.exists(json_file):
print(f"[MISS] {json_file}")
return
if not os.path.isdir(img_dir):
print(f"[MISS] image dir {img_dir}")
return
data = json.load(open(json_file, "r", encoding="utf-8"))
W, H = data["width"], data["height"]
layers = data["layers"]
# ---- 解析规则 (沿用 test.py 的逻辑) ----
if ":" in rule:
top_name, base_layer_name = rule.split(":")
order = [base_layer_name, top_name]
else:
order = [rule]
# diff模式特殊处理
if mode == "diff":
# 对于 diff 模式,需要找到母图
base_candidate = rule[0] # Bb -> B
# 把母图加在前面
if find_layer(layers, base_candidate):
order = [base_candidate] + order
print(f"[DO] {out_name}: {' + '.join(order)}")
canvas = Image.new("RGBA", (W, H), (0, 0, 0, 0))
for lname in order:
layer = find_layer(layers, lname)
draw_layer(canvas, img_dir, layer)
os.makedirs(EXPORT_DIR, exist_ok=True)
out_png = os.path.join(EXPORT_DIR, f"{out_name}.png")
canvas.save(out_png)
def main():
csv_file = "imagediffmap.csv"
if not os.path.exists(csv_file):
print(f"[ERROR] {csv_file} not found!")
return
# 用正则匹配 ev 开头的规则行
pattern = re.compile(r'^(ev\w+),(ev\w+),(seton|diff),([^,]+)$')
rules = []
with open(csv_file, "r", encoding="utf-16") as f:
for line in f:
line = line.strip()
if not line:
continue
match = pattern.match(line)
if match:
rules.append(line)
if not rules:
print("[WARN] No ev rules found!")
return
print(f"Found {len(rules)} rules.")
for line in rules:
try:
process_rule(line)
except Exception as e:
print(f"[ERR] {line}: {e}")
print("\n[DONE]")
if __name__ == "__main__":
main()
经过测试,对千恋万花、天使☆嚣嚣、星光咖啡馆与死神之蝶都是可行的。
LimeLight Lemonade应该也是可以的,它在json里就标记了母图,会更简单。
最后,0721快乐,Ciallo~(∠・ω< )⌒☆
