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>
This commit is contained in:
+119
@@ -0,0 +1,119 @@
|
||||
# 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)
|
||||
Reference in New Issue
Block a user