Files

258 lines
8.5 KiB
GDScript
Raw Permalink Normal View History

# De-mirrors Lena's limb UVs (v2 — triangle-level split).
# Her left arm/leg share the right side's UV islands (arms 100%, legs 99%), so
# any baked tattoo prints on both sides. v1 moved whole islands and left 4%/29%
# residual overlap (mixed torso+limb islands stayed put). v2 moves exactly the
# triangles a left-limb bake would paint (max left-chain weight ≥ 0.28), splits
# mixed islands by duplicating boundary vertices (seam is invisible: positions/
# normals identical, texels copied 1:1), widens the atlas 4096→8192 and packs
# the moved pieces into the new right half. Exports a modified GLB.
# Run headless:
# tinqs.console.exe --headless --path tattoo-test -s res://demirror_lena.gd
extends SceneTree
const GLB_IN := "C:/Users/CAN/tinqs-ltd/ariki-game/assets/quaternius/derived-bodies/Ariki_Female_QuatSkin.glb"
const GLB_OUT := "C:/Users/CAN/tinqs-ltd/tattoo-test/lena_demirrored.glb"
const MOVE_W := 0.28 # move tris whose max left-chain weight ≥ this (bake gate is 0.30)
const PAD := 16 # texel padding around moved pieces
var skel: Skeleton3D
func _init() -> void:
var doc := GLTFDocument.new()
var state := GLTFState.new()
if doc.append_from_file(GLB_IN, state) != OK:
push_error("GLB load failed")
quit(1)
return
var scene := doc.generate_scene(state)
var body: MeshInstance3D = 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 and String(n.name).to_lower().contains("lena"):
body = n
for c in n.get_children():
stack.push_back(c)
if skel == null or body == null:
push_error("skeleton or Lena mesh not found")
quit(1)
return
var mesh: ArrayMesh = body.mesh
var arrays: Array = mesh.surface_get_arrays(0)
var verts: PackedVector3Array = arrays[Mesh.ARRAY_VERTEX]
var norms: PackedVector3Array = arrays[Mesh.ARRAY_NORMAL]
var tangents: PackedFloat32Array = arrays[Mesh.ARRAY_TANGENT] if arrays[Mesh.ARRAY_TANGENT] != null else PackedFloat32Array()
var uvs: PackedVector2Array = arrays[Mesh.ARRAY_TEX_UV]
var vbones: PackedInt32Array = arrays[Mesh.ARRAY_BONES]
var vweights: PackedFloat32Array = arrays[Mesh.ARRAY_WEIGHTS]
var indices: PackedInt32Array = arrays[Mesh.ARRAY_INDEX]
var nverts := verts.size()
var influences := int(float(vbones.size()) / float(nverts))
var orig_uvs := uvs.duplicate()
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)
# per-vertex combined LEFT-limb weight (arm incl. fingers + leg incl. ball)
var left_bones := ["upperarm_l", "lowerarm_l", "hand_l",
"thigh_l", "calf_l", "foot_l", "ball_l", "ball_leaf_l"]
for f in ["index", "middle", "pinky", "ring", "thumb"]:
for seg in ["01", "02", "03", "04_leaf"]:
left_bones.append("%s_%s_l" % [f, seg])
var left_binds := _bind_set(bind_bone, left_bones)
var lw := PackedFloat32Array()
lw.resize(nverts)
for v in nverts:
for k in influences:
if left_binds.has(vbones[v * influences + k]):
lw[v] += vweights[v * influences + k]
# moved triangles = what a left-limb bake would touch
var ntris := indices.size() / 3
var tri_moved := PackedByteArray()
tri_moved.resize(ntris)
var n_moved_tris := 0
for t in ntris:
var w := maxf(lw[indices[t * 3]], maxf(lw[indices[t * 3 + 1]], lw[indices[t * 3 + 2]]))
if w >= MOVE_W:
tri_moved[t] = 1
n_moved_tris += 1
print("moved tris: %d / %d" % [n_moved_tris, ntris])
# vertex usage: moved-only, unmoved-only, or boundary (both)
var used_moved := PackedByteArray()
used_moved.resize(nverts)
var used_unmoved := PackedByteArray()
used_unmoved.resize(nverts)
for t in ntris:
for j in 3:
var v := indices[t * 3 + j]
if tri_moved[t] == 1:
used_moved[v] = 1
else:
used_unmoved[v] = 1
# connected components of moved tris (via shared verts)
var parent := PackedInt32Array()
parent.resize(nverts)
for v in nverts:
parent[v] = v
for t in ntris:
if tri_moved[t] == 0:
continue
var a := _find(parent, indices[t * 3])
var b := _find(parent, indices[t * 3 + 1])
var c := _find(parent, indices[t * 3 + 2])
if b != a:
parent[b] = a
if c != a:
parent[c] = a
# duplicate boundary verts; moved tris rebind to the copies
var dup_of := {}
var comp_of_new := {} # new/moved vert index -> component root
for t in ntris:
if tri_moved[t] == 0:
continue
var root := _find(parent, indices[t * 3])
for j in 3:
var v := indices[t * 3 + j]
if used_unmoved[v] == 1:
if not dup_of.has(v):
var nv := verts.size()
verts.append(verts[v])
norms.append(norms[v])
if tangents.size() > 0:
for k in 4:
tangents.append(tangents[v * 4 + k])
uvs.append(uvs[v])
orig_uvs.append(orig_uvs[v])
for k in influences:
vbones.append(vbones[v * influences + k])
vweights.append(vweights[v * influences + k])
dup_of[v] = nv
comp_of_new[nv] = root
indices[t * 3 + j] = dup_of[v]
else:
comp_of_new[v] = root
print("boundary verts duplicated: ", dup_of.size())
# source texture → widened atlas
var mat: BaseMaterial3D = mesh.surface_get_material(0)
var img: Image = mat.albedo_texture.get_image()
img.clear_mipmaps()
img.convert(Image.FORMAT_RGBA8)
var tw := img.get_width()
var th := img.get_height()
var wide := Image.create(tw * 2, th, false, Image.FORMAT_RGBA8)
wide.blit_rect(img, Rect2i(0, 0, tw, th), Vector2i.ZERO)
# everyone's u compresses into the left half
for v in uvs.size():
uvs[v].x *= 0.5
# component bboxes in ORIGINAL texel space
var comps := {}
for v in comp_of_new:
var root: int = comp_of_new[v]
if not comps.has(root):
comps[root] = {"verts": [], "mn": Vector2(1e9, 1e9), "mx": Vector2(-1e9, -1e9)}
comps[root]["verts"].append(v)
comps[root]["mn"] = (comps[root]["mn"] as Vector2).min(orig_uvs[v])
comps[root]["mx"] = (comps[root]["mx"] as Vector2).max(orig_uvs[v])
var pieces := []
for root in comps:
var m: Dictionary = comps[root]
m["px0"] = maxi(int(m["mn"].x * tw) - PAD, 0)
m["py0"] = maxi(int(m["mn"].y * th) - PAD, 0)
m["px1"] = mini(int(m["mx"].x * tw) + PAD, tw - 1)
m["py1"] = mini(int(m["mx"].y * th) + PAD, th - 1)
m["h"] = m["py1"] - m["py0"]
pieces.append(m)
pieces.sort_custom(func(a, b): return a["h"] > b["h"])
print("moved pieces: ", pieces.size())
# shelf-pack into the right half, blit texels, remap UVs
var cursor_x := tw + PAD
var cursor_y := PAD
var row_h := 0
for m in pieces:
var w_px: int = m["px1"] - m["px0"]
var h_px: int = m["py1"] - m["py0"]
if cursor_x + w_px >= tw * 2 - PAD:
cursor_x = tw + PAD
cursor_y += row_h + PAD
row_h = 0
if cursor_y + h_px >= th - PAD:
push_error("packing overflow — atlas half too small")
quit(1)
return
wide.blit_rect(img, Rect2i(m["px0"], m["py0"], w_px, h_px),
Vector2i(cursor_x, cursor_y))
for v in m["verts"]:
var opx: float = orig_uvs[v].x * tw
var opy: float = orig_uvs[v].y * th
uvs[v] = Vector2(
(cursor_x + (opx - m["px0"])) / (tw * 2.0),
(cursor_y + (opy - m["py0"])) / float(th))
cursor_x += w_px + PAD
row_h = maxi(row_h, h_px)
# rebuild surface + texture, export
arrays[Mesh.ARRAY_VERTEX] = verts
arrays[Mesh.ARRAY_NORMAL] = norms
if tangents.size() > 0:
arrays[Mesh.ARRAY_TANGENT] = tangents
arrays[Mesh.ARRAY_TEX_UV] = uvs
arrays[Mesh.ARRAY_BONES] = vbones
arrays[Mesh.ARRAY_WEIGHTS] = vweights
arrays[Mesh.ARRAY_INDEX] = indices
var new_mesh := ArrayMesh.new()
new_mesh.add_surface_from_arrays(Mesh.PRIMITIVE_TRIANGLES, arrays)
mat.albedo_texture = ImageTexture.create_from_image(wide)
new_mesh.surface_set_material(0, mat)
body.mesh = new_mesh
var out_doc := GLTFDocument.new()
var out_state := GLTFState.new()
if out_doc.append_from_scene(scene, out_state) != OK:
push_error("export append failed")
quit(1)
return
if out_doc.write_to_filesystem(out_state, GLB_OUT) != OK:
push_error("export write failed")
quit(1)
return
print("verts %d -> %d, atlas %dx%d" % [nverts, verts.size(), tw * 2, th])
print("saved: ", GLB_OUT)
quit()
func _bind_set(bind_bone: PackedInt32Array, names: Array) -> Dictionary:
var bone_ids := {}
for bn in names:
var idx := skel.find_bone(bn)
if idx < 0:
push_error("bone not found: " + str(bn))
bone_ids[idx] = true
var out := {}
for i in bind_bone.size():
if bone_ids.has(bind_bone[i]):
out[i] = true
return out
func _find(parent: PackedInt32Array, v: int) -> int:
var r := v
while parent[r] != r:
parent[r] = parent[parent[r]]
r = parent[r]
return r