(不用AI)0721特刊!关于柚子社的CG合成!
卿酱kimi
2026年07月21日 17:54

今天的内容是柚子社作品的CG合成[打call]

实际上,相较于前几刊,柚子社的CG合成可谓不费吹灰之力!

本文的创新点是,通过imagediffmap.csv避免枚举所有cg合成组合,并复刻游戏原有顺序和名称,而非使用AI对齐工具、或是使用合成程序穷举。

按照惯例,下文会分成坐标解析,发现组合规则和实操三部分!

一、找到坐标信息

与其它引擎不同,krkr引擎往往将每一组图片及其坐标封装在同一个pimg里。无论是使用krkrdump还是用Garbro2,都可以轻松的获取,在此不赘述,站内已有大量攻略。不过,本文使用的是krkrdump,并且推荐使用krkrdump,可以免去找imagediffmap.csv的麻烦。

无论如何,如图准备好您要的pimg即可。

为了欢庆0721加急制备

然后使用FreeMote​里的Psbdecompile.exe

如果要移动程序位置,注意将lib文件夹一并复制。

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

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

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

而这是先前PsbDescompile的输出:

7、8、9、10、11、12、13、14、15、16、17、18、23、24、25、26

对比发现,json文件首先给图片以代号,然后指定了每个图层的偏移和属性。 这正是我们合成时需要的图层偏移量。以上的内容前人已有充分的探讨,如有所忽略还请海涵。

二、解决底图、母图的区分问题

前文只是知道了每个图片的内部代号和自带的位置信息,但我们仍不知道引擎如何判定一张图片母图还是底图,以及选择哪两张母图、底图。

重新翻阅后发现,这一关键信息位于imagediffmap.csv。此文件就在krkrdump的输出目录当中,如果没有在输出目录中看见,可能是由于krkrdump的配置文件中写了过滤器,推荐去github重下。默认应当是不过滤的。

仔细辨认后发现,文件中有两种语法,diff和seton。不过,在我截的图范围之外,diff几乎没有出现,推测为引擎使用的某种特殊语法。跳过diff语法不会影响我们的运行效果,因此重点讨论seton。以以下两句为例:

代码块
PlainText
自动换行
复制代码
ev111ba,ev111a,seton,Ba
复制成功

含义是:目标名称为ev111ba,目标属于ev111a系列,将Ba(json中赋予的代号)作为底图,完成。——也就是说,原封不动Ba。

代码块
PlainText
自动换行
复制代码
ev111bb,ev111a,seton,Bb:Ba
复制成功

含义是:目标名称为ev111bb,目标属于ev111a系列,将Ba(json中赋予的代号)作为底图,将Bb叠加其上。

综上,就是krkr引擎渲染的指定逻辑。

至此,已经有了合成所需要的所有信息。之后的合成如前几期,仍是底图-叠层的模式。可以开写了!

三、程序

写程序的时候为保证方便性,前文“pimg拖动到PsbDecompile.exe”后不用做任何操作,将imagediffmap.csv放入该文件夹,程序也在此执行。我加入了多任务处理,合成速度在多核设备会明显提升。

代码块
PlainText
自动换行
复制代码
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~(∠・ω< )⌒☆

这是原图