# Composites the v4 mask-fitted marks into the Quaternius base-color texture: # maps each gen-v4 result back into UV space (inverse of prep.gd's transform), # extracts ink by luminance, multiplies it onto skin inside the part mask, then # dilates 4px into unpainted texels so UV-seam bilinear/mip sampling never shows # a bare gutter line. Backs up the previous bake to baked_body_v3.png once. # Run headless (after _gen-marks-v4.ps1): # tinqs.console.exe --headless --path tattoo-test -s res://composite.gd extends SceneTree const SKIN_TEX_PATH := "C:/Users/CAN/tinqs-ltd/ariki-game/assets/quaternius/source-models/T_Regular_Male_Dark_BaseColor_png.png" const MASK_DIR := "C:/Users/CAN/tinqs-ltd/tattoo-test/masks/" const GEN_DIR := "C:/Users/CAN/tinqs-ltd/tattoo-test/gen-v4/" const OUT_PATH := "C:/Users/CAN/tinqs-ltd/tattoo-test/baked_body.png" const BACKUP_PATH := "C:/Users/CAN/tinqs-ltd/tattoo-test/baked_body_v3.png" const INK_COLOR := Color(0.10, 0.085, 0.08) const INK_OPACITY := 0.88 const DILATE := 4 const JOBS := [ {"part": "arm_r", "mark": "chiefs-mark"}, {"part": "arm_l", "mark": "iron-spine"}, {"part": "leg_r", "mark": "voyagers-current"}, {"part": "leg_l", "mark": "storm-bearer"}, {"part": "chest", "mark": "star-eye"}, {"part": "back", "mark": "hearth-warmth"}, ] func _init() -> void: var prep := load("res://prep.gd") if FileAccess.file_exists(OUT_PATH) and not FileAccess.file_exists(BACKUP_PATH): DirAccess.copy_absolute(OUT_PATH, BACKUP_PATH) print("backed up previous bake -> ", BACKUP_PATH) var img := Image.load_from_file(SKIN_TEX_PATH) img.convert(Image.FORMAT_RGBA8) var size := img.get_width() var painted := PackedByteArray() painted.resize(size * size) var missing := 0 for job in JOBS: var gen_path: String = GEN_DIR + "v4_" + job["part"] + "_" + job["mark"] + ".png" if not FileAccess.file_exists(gen_path): push_error("missing gen result: " + gen_path + " (run _gen-marks-v4.ps1 first)") missing += 1 continue var mask := Image.load_from_file(MASK_DIR + "mask_" + job["part"] + ".png") var gen := Image.load_from_file(gen_path) gen.convert(Image.FORMAT_RGBA8) var t: Dictionary = prep.transform_for(mask) var rect: Rect2i = t["rect"] var s: float = t["scale"] var off: Vector2 = t["offset"] var out_scale := float(gen.get_width()) / float(prep.CANVAS) var mask_scale := float(mask.get_width()) / float(size) var count := 0 for py in range(rect.position.y, rect.end.y): for px in range(rect.position.x, rect.end.x): var mx := int(px * mask_scale) var my := int(py * mask_scale) if mask.get_pixel(mx, my).r < 0.5: continue var cx := ((px - rect.position.x) + 0.5) * s + off.x var cy := ((py - rect.position.y) + 0.5) * s + off.y var c := _sample_bilinear(gen, cx * out_scale, cy * out_scale) var ink := 1.0 - smoothstep(0.45, 0.80, (c.r + c.g + c.b) / 3.0) var alpha := ink * INK_OPACITY if alpha > 0.003: img.set_pixel(px, py, img.get_pixel(px, py).lerp(INK_COLOR, alpha)) count += 1 painted[py * size + px] = 1 # whole mask counts as painted (bare skin is intentional) print("%s %s painted px: %d" % [job["part"], job["mark"], count]) if missing == JOBS.size(): push_error("no gen results found - nothing composited") quit(1) return # edge padding: pull composited colors DILATE px outward into unpainted texels for pass_i in DILATE: var grown := PackedInt32Array() for y in size: for x in size: if painted[y * size + x] == 1: continue var acc := Color(0, 0, 0) var n := 0 for d in [[1, 0], [-1, 0], [0, 1], [0, -1]]: var nx: int = x + d[0] var ny: int = y + d[1] if nx < 0 or nx >= size or ny < 0 or ny >= size: continue if painted[ny * size + nx] == 1: acc += img.get_pixel(nx, ny) n += 1 if n > 0: img.set_pixel(x, y, acc / n) grown.append(y * size + x) for p in grown: painted[p] = 1 img.save_png(OUT_PATH) print("saved: ", OUT_PATH) quit() func _sample_bilinear(img: Image, x: float, y: float) -> Color: var w := img.get_width() var h := img.get_height() var fx := clampf(x - 0.5, 0.0, w - 1.001) var fy := clampf(y - 0.5, 0.0, h - 1.001) var x0 := int(fx) var y0 := int(fy) var x1 := mini(x0 + 1, w - 1) var y1 := mini(y0 + 1, h - 1) var tx := fx - x0 var ty := fy - y0 return img.get_pixel(x0, y0).lerp(img.get_pixel(x1, y0), tx) \ .lerp(img.get_pixel(x0, y1).lerp(img.get_pixel(x1, y1), tx), ty)