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
|