File size: 6,198 Bytes
84e8204
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
extends Node
##
## RiggingModule - armature creation and automatic vertex weighting (Features 41-60).
##
## Creates a Skeleton3D, lets the user add bones as a transform hierarchy, and
## binds skinning data (ARRAY_BONES + ARRAY_WEIGHTS) to a MeshInstance3D using
## inverse-distance weighting against bone segments. The result is a normal,
## engine-native skinned mesh - no custom shader needed.
##

## Bone definitions the tool builds. Each entry: { name, parent, position, tail }
var _bones: Array[Dictionary] = []
var _skeleton: Skeleton3D = null
var _host: Node = null
var _bound_mesh: MeshInstance3D = null


func _activate(host: Node) -> void:
	_host = host
	StudioBus.object_selected.connect(_on_object_selected)
	print("[Rigging] activated")


func _deactivate() -> void:
	if StudioBus.object_selected.is_connected(_on_object_selected):
		StudioBus.object_selected.disconnect(_on_object_selected)
	_host = null


func _on_object_selected(node: Node3D) -> void:
	_bound_mesh = node as MeshInstance3D


## --- Armature creation ------------------------------------------------------

## Add a bone. parent="" means root. Returns the new bone index.
func add_bone(name: String, parent: String, position: Vector3, tail: Vector3 = Vector3.ZERO) -> int:
	_ensure_skeleton()
	var parent_idx := -1
	if parent != "":
		parent_idx = _find_bone(parent)
	var idx := _skeleton.get_bone_count()
	_skeleton.add_bone(name)
	if parent_idx >= 0:
		_skeleton.set_bone_parent(idx, parent_idx)
	var rest := Transform3D()
	# Point the bone's -Z toward its tail so rotations look natural.
	var dir := (tail - position)
	if dir.length_squared() > 0.000001:
		rest = Transform3D(_look_at_basis(dir.normalized()), position)
	else:
		rest = Transform3D(Basis.IDENTITY, position)
	_skeleton.set_bone_rest(idx, rest)
	_skeleton.set_bone_pose_position(idx, position)
	_bones.append({"name": name, "parent": parent, "index": idx})
	StudioBus.scene_modified.emit()
	return idx


func _ensure_skeleton() -> void:
	if _skeleton != null and is_instance_valid(_skeleton):
		return
	_skeleton = Skeleton3D.new()
	_skeleton.name = "Armature"
	if _host != null:
		_host.get_node_or_null("Rig").add_child(_skeleton) if _host.has_node("Rig") else _host.add_child(_skeleton)


func _find_bone(name: String) -> int:
	if _skeleton == null:
		return -1
	for i in range(_skeleton.get_bone_count()):
		if _skeleton.get_bone_name(i) == name:
			return i
	return -1


func _look_at_basis(dir: Vector3) -> Basis:
	var up := Vector3.UP
	if absf(dir.dot(up)) > 0.99:
		up = Vector3.RIGHT
	var right := dir.cross(up).normalized()
	var real_up := right.cross(dir).normalized()
	return Basis(right, real_up, -dir)


## Build a simple starter humanoid-ish spine + limbs for a mesh's bounds.
func generate_default_armature(mesh: MeshInstance3D) -> void:
	if mesh == null or mesh.mesh == null:
		return
	var size := mesh.mesh.get_aabb().size
	var h := size.y
	add_bone("root", "", Vector3.ZERO, Vector3(0, h * 0.1, 0))
	add_bone("spine", "root", Vector3(0, h * 0.1, 0), Vector3(0, h * 0.35, 0))
	add_bone("chest", "spine", Vector3(0, h * 0.35, 0), Vector3(0, h * 0.6, 0))
	add_bone("neck", "chest", Vector3(0, h * 0.6, 0), Vector3(0, h * 0.72, 0))
	add_bone("head", "neck", Vector3(0, h * 0.72, 0), Vector3(0, h * 0.95, 0))
	add_bone("arm_l", "chest", Vector3(-size.x * 0.4, h * 0.6, 0), Vector3(-size.x * 0.9, h * 0.45, 0))
	add_bone("arm_r", "chest", Vector3(size.x * 0.4, h * 0.6, 0), Vector3(size.x * 0.9, h * 0.45, 0))
	add_bone("leg_l", "root", Vector3(-size.x * 0.2, 0, 0), Vector3(-size.x * 0.2, -h * 0.5, 0))
	add_bone("leg_r", "root", Vector3(size.x * 0.2, 0, 0), Vector3(size.x * 0.2, -h * 0.5, 0))
	bind_weights(mesh)


## --- Automatic weight binding -----------------------------------------------

## Compute skinning arrays using inverse-distance to bone segments and apply.
## `max_influences` must be <= 4 for the standard mobile skinning path.
func bind_weights(mesh: MeshInstance3D, max_influences: int = 4) -> void:
	if mesh == null or _skeleton == null or mesh.mesh == null or mesh.mesh.get_surface_count() == 0:
		return
	var arr := mesh.mesh.surface_get_arrays(0)
	var verts: PackedVector3Array = arr[Mesh.ARRAY_VERTEX]
	var n := verts.size()
	var bone_count := _skeleton.get_bone_count()
	var bone_segments: Array = []
	for i in range(bone_count):
		var pos := _skeleton.get_bone_global_rest(i).origin
		bone_segments.append({"idx": i, "pos": pos})

	var vbones := PackedInt32Array()
	var vweights := PackedFloat32Array()
	vbones.resize(n * 4)
	vweights.resize(n * 4)
	var to_mesh := mesh.global_transform.affine_inverse()

	for vi in range(n):
		var p := verts[vi]
		# Rank bones by distance to their head position.
		var dists: Array = []
		for b in bone_segments:
			var local_bone: Vector3 = to_mesh * (mesh.global_transform * b["pos"])
			dists.append({"idx": b["idx"], "d": p.distance_to(local_bone)})
		dists.sort_custom(func(a, b): return a["d"] < b["d"])
		var influ: int = mini(max_influences, dists.size())
		var wsum := 0.0
		var ws: Array[float] = []
		for k in range(influ):
			var d: float = maxf(0.0001, dists[k]["d"])
			var w := 1.0 / (d * d)
			ws.append(w)
			wsum += w
		for slot in range(4):
			var base := vi * 4 + slot
			if slot < influ and wsum > 0.0:
				vbones[base] = dists[slot]["idx"]
				vweights[base] = ws[slot] / wsum
			else:
				vbones[base] = 0
				vweights[base] = 0.0

	# Rebuild the mesh surface with skinning channels added.
	arr[Mesh.ARRAY_BONES] = vbones
	arr[Mesh.ARRAY_WEIGHTS] = vweights
	var new_mesh := MeshPool.acquire_mesh()
	new_mesh.add_surface_from_arrays(Mesh.PRIMITIVE_TRIANGLES, arr)
	mesh.mesh = new_mesh
	mesh.skeleton = mesh.get_path_to(_skeleton)
	# A Skin must be bound so the renderer knows which skeleton supplies the
	# bone transforms for these indices. add_bind(bone_index, rest_pose).
	var skin := Skin.new()
	for i in range(bone_count):
		skin.add_bind(i, _skeleton.get_bone_global_rest(i))
	mesh.skin = skin
	StudioBus.scene_modified.emit()
	print("[Rigging] bound %d verts to %d bones" % [n, bone_count])


func bone_count() -> int:
	return _skeleton.get_bone_count() if _skeleton != null else 0


func get_skeleton() -> Skeleton3D:
	return _skeleton