Files
Can Narin ce4c4e7d13 Tattoo test bed: Quaternius v3-v6 bakes + Lena pipeline (bake, de-mirror, re-UV, viewers)
Session 2026-07-23/24 additions on top of the existing Quaternius bed:
- bake_lena.gd: 6-piece bake on QuatSkin Lena (chiefs-mark/iron-spine arms,
  voyagers/storm-bearer legs, star-eye chest, hearth-warmth back); global-axis
  wrap frame + 3cm junction blend kill the elbow split line
- demirror_lena.gd: tri-level split of mirror-shared limb UVs (arms 100%->0%,
  legs 99%->5% crotch-only residual)
- reuv_lena.gd: Quaternius-style semantic re-UV (16 islands, cylinder limbs +
  cylinder head/torso with planar caps) + texture transfer; fixes armpit/chin/
  cheek planar smears; single 4096 atlas, per-side tattoos native
- main_lena / anim_lena viewers (18 UAL clips, U light toggle) + probes + shots

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-24 00:47:12 +03:00

120 lines
4.4 KiB
GDScript

# 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)