Download scripts/modules/rigging.gd from geter62/monkeystudio: direct link, hf CLI and curl.
- Browser
- Download file 6.2 kB
-
https://huggingface.co/geter62/monkeystudio/resolve/main/scripts/modules/rigging.gd
- Command line
-
hf download hf://geter62/monkeystudio/scripts/modules/rigging.gd
-
curl -L -o rigging.gd https://huggingface.co/geter62/monkeystudio/resolve/main/scripts/modules/rigging.gd
6.2 kB
| 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 | |