120 lines
4.4 KiB
GDScript
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)
|