258 lines
8.5 KiB
GDScript
258 lines
8.5 KiB
GDScript
|
|
# 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
|