ce4c4e7d13
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>
166 lines
6.1 KiB
GDScript
166 lines
6.1 KiB
GDScript
# Renders the Quaternius Regular Male UV layout as a labeled template:
|
|
# - fills each texel by body region (dominant bone-weight group)
|
|
# - draws triangle wireframe edges on top
|
|
# Output: uv_template.png (2048², same space as T_Regular_Male_Dark_BaseColor)
|
|
# Run headless:
|
|
# tinqs.console.exe --headless --path tattoo-test -s res://uv_template.gd
|
|
extends SceneTree
|
|
|
|
const GLTF_PATH := "C:/Users/CAN/tinqs-ltd/ariki-game/assets/quaternius/source-models/Regular_Male_FullBody.gltf"
|
|
const OUT_PATH := "C:/Users/CAN/tinqs-ltd/tattoo-test/uv_template.png"
|
|
const SIZE := 2048
|
|
|
|
# region -> [bone name prefixes], painted in listed order (later wins ties by weight)
|
|
const REGIONS := [
|
|
{"name": "head", "bones": ["head", "neck_01"], "col": Color(0.85, 0.75, 0.60)},
|
|
{"name": "torso", "bones": ["spine_01", "spine_02", "spine_03", "pelvis", "clavicle_l", "clavicle_r"], "col": Color(0.95, 0.85, 0.70)},
|
|
{"name": "arm_r", "bones": ["upperarm_r", "lowerarm_r"], "col": Color(0.90, 0.55, 0.45)},
|
|
{"name": "arm_l", "bones": ["upperarm_l", "lowerarm_l"], "col": Color(0.55, 0.70, 0.90)},
|
|
{"name": "hand_r", "bones": ["hand_r", "thumb_", "index_", "middle_", "ring_", "pinky_"], "col": Color(0.75, 0.40, 0.35)},
|
|
{"name": "hand_l", "bones": ["hand_l"], "col": Color(0.40, 0.55, 0.75)},
|
|
{"name": "leg_r", "bones": ["thigh_r", "calf_r"], "col": Color(0.90, 0.75, 0.40)},
|
|
{"name": "leg_l", "bones": ["thigh_l", "calf_l"], "col": Color(0.55, 0.80, 0.55)},
|
|
{"name": "foot_r", "bones": ["foot_r", "ball_r"], "col": Color(0.75, 0.60, 0.30)},
|
|
{"name": "foot_l", "bones": ["foot_l", "ball_l"], "col": Color(0.40, 0.65, 0.40)},
|
|
]
|
|
const EDGE_COL := Color(0.15, 0.15, 0.18)
|
|
|
|
var skel: Skeleton3D
|
|
|
|
func _init() -> void:
|
|
var doc := GLTFDocument.new()
|
|
var state := GLTFState.new()
|
|
if doc.append_from_file(GLTF_PATH, state) != OK:
|
|
push_error("GLTF load failed")
|
|
quit(1)
|
|
return
|
|
var scene := doc.generate_scene(state)
|
|
|
|
var body: Node = null
|
|
var stack: Array = [scene]
|
|
while not stack.is_empty():
|
|
var n: Node = stack.pop_back()
|
|
if n is Skeleton3D and skel == null:
|
|
skel = n
|
|
if (n is MeshInstance3D or n.get_class() == "ImporterMeshInstance3D") \
|
|
and String(n.name).to_lower().contains("regularmale"):
|
|
body = n
|
|
for c in n.get_children():
|
|
stack.push_back(c)
|
|
if skel == null or body == null:
|
|
push_error("skeleton or body mesh not found")
|
|
quit(1)
|
|
return
|
|
|
|
var mesh = body.mesh
|
|
if mesh is ImporterMesh:
|
|
mesh = mesh.get_mesh()
|
|
var arrays: Array = mesh.surface_get_arrays(0)
|
|
var uvs: PackedVector2Array = arrays[Mesh.ARRAY_TEX_UV]
|
|
var vbones: PackedInt32Array = arrays[Mesh.ARRAY_BONES]
|
|
var vweights = arrays[Mesh.ARRAY_WEIGHTS]
|
|
var indices: PackedInt32Array = arrays[Mesh.ARRAY_INDEX]
|
|
var nverts := uvs.size()
|
|
var influences := int(float(vbones.size()) / float(nverts))
|
|
|
|
var skin: Skin = body.skin
|
|
var bind_bone := PackedInt32Array()
|
|
for i in skin.get_bind_count():
|
|
var b := skin.get_bind_bone(i)
|
|
if b < 0:
|
|
b = skel.find_bone(skin.get_bind_name(i))
|
|
bind_bone.append(b)
|
|
|
|
# bind index -> region index (by bone-name prefix match)
|
|
var bind_region := PackedInt32Array()
|
|
for i in bind_bone.size():
|
|
var bname := skel.get_bone_name(bind_bone[i])
|
|
var reg := -1
|
|
for r in REGIONS.size():
|
|
for prefix in REGIONS[r]["bones"]:
|
|
if bname.begins_with(prefix):
|
|
reg = r
|
|
break
|
|
if reg >= 0:
|
|
break
|
|
bind_region.append(reg)
|
|
|
|
# per-vertex dominant region
|
|
var vert_region := PackedInt32Array()
|
|
vert_region.resize(nverts)
|
|
for v in nverts:
|
|
var acc := {}
|
|
for k in influences:
|
|
var r := bind_region[vbones[v * influences + k]]
|
|
if r >= 0:
|
|
acc[r] = acc.get(r, 0.0) + vweights[v * influences + k]
|
|
var best := -1
|
|
var best_w := 0.0
|
|
for r in acc:
|
|
if acc[r] > best_w:
|
|
best_w = acc[r]
|
|
best = r
|
|
vert_region[v] = best
|
|
|
|
var img := Image.create(SIZE, SIZE, false, Image.FORMAT_RGBA8)
|
|
img.fill(Color(1, 1, 1))
|
|
|
|
# fill pass: flat region color per triangle (majority vertex region)
|
|
var ntris := indices.size() / 3
|
|
for t in ntris:
|
|
var i0 := indices[t * 3]
|
|
var i1 := indices[t * 3 + 1]
|
|
var i2 := indices[t * 3 + 2]
|
|
var reg := vert_region[i0]
|
|
if vert_region[i1] == vert_region[i2] and vert_region[i1] != -1:
|
|
reg = vert_region[i1]
|
|
var col: Color = Color(0.8, 0.8, 0.8) if reg < 0 else REGIONS[reg]["col"]
|
|
_fill_tri(img, uvs[i0], uvs[i1], uvs[i2], col)
|
|
|
|
# wireframe pass
|
|
for t in ntris:
|
|
var i0 := indices[t * 3]
|
|
var i1 := indices[t * 3 + 1]
|
|
var i2 := indices[t * 3 + 2]
|
|
_line(img, uvs[i0], uvs[i1])
|
|
_line(img, uvs[i1], uvs[i2])
|
|
_line(img, uvs[i2], uvs[i0])
|
|
|
|
img.save_png(OUT_PATH)
|
|
print("saved: ", OUT_PATH)
|
|
for r in REGIONS.size():
|
|
print(" region %-7s -> %s" % [REGIONS[r]["name"], REGIONS[r]["col"].to_html(false)])
|
|
quit()
|
|
|
|
func _fill_tri(img: Image, a: Vector2, b: Vector2, c: Vector2, col: Color) -> void:
|
|
var p0 := a * SIZE
|
|
var p1 := b * SIZE
|
|
var p2 := c * SIZE
|
|
var denom := (p1.y - p2.y) * (p0.x - p2.x) + (p2.x - p1.x) * (p0.y - p2.y)
|
|
if absf(denom) < 1e-9:
|
|
return
|
|
var minx := clampi(int(floor(min(p0.x, min(p1.x, p2.x)))), 0, SIZE - 1)
|
|
var maxx := clampi(int(ceil(max(p0.x, max(p1.x, p2.x)))), 0, SIZE - 1)
|
|
var miny := clampi(int(floor(min(p0.y, min(p1.y, p2.y)))), 0, SIZE - 1)
|
|
var maxy := clampi(int(ceil(max(p0.y, max(p1.y, p2.y)))), 0, SIZE - 1)
|
|
for py in range(miny, maxy + 1):
|
|
for px in range(minx, maxx + 1):
|
|
var fx := px + 0.5
|
|
var fy := py + 0.5
|
|
var b0 := ((p1.y - p2.y) * (fx - p2.x) + (p2.x - p1.x) * (fy - p2.y)) / denom
|
|
var b1 := ((p2.y - p0.y) * (fx - p2.x) + (p0.x - p2.x) * (fy - p2.y)) / denom
|
|
var b2 := 1.0 - b0 - b1
|
|
if b0 < -0.02 or b1 < -0.02 or b2 < -0.02:
|
|
continue
|
|
img.set_pixel(px, py, col)
|
|
|
|
func _line(img: Image, a: Vector2, b: Vector2) -> void:
|
|
var p0 := a * SIZE
|
|
var p1 := b * SIZE
|
|
var steps := int(maxf(absf(p1.x - p0.x), absf(p1.y - p0.y))) + 1
|
|
for s in steps + 1:
|
|
var p := p0.lerp(p1, float(s) / float(steps))
|
|
var px := clampi(int(p.x), 0, SIZE - 1)
|
|
var py := clampi(int(p.y), 0, SIZE - 1)
|
|
img.set_pixel(px, py, EDGE_COL)
|