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