extends RefCounted ## Converts Skeleton3D-skinned meshes into NON-skinned mesh pieces parented to ## BoneAttachment3D nodes, so rendering never uses GPU vertex-skinning (transform ## feedback). Required on ANGLE/Vulkan/Mali mobile GPUs (e.g. Mali-G720 in mobile ## Chrome/Brave), where Godot's Compatibility skinning path renders skinned meshes ## invisible while non-skinned meshes draw fine. ## ## Each triangle is assigned wholesale to its dominant bone (highest summed weight), ## so segments stay watertight; joints become rigid (no smooth bending), which suits ## the low-poly look. Call RigidSkin.convert_tree(character_root) once after the scene ## is instanced (bones don't need to be posed yet — pieces are baked in bind space). static func convert_tree(root: Node) -> int: var converted := 0 for skel: Skeleton3D in _find(root, "Skeleton3D", []): for mi: Node in skel.get_children(): if mi is MeshInstance3D and (mi as MeshInstance3D).skin != null \ and (mi as MeshInstance3D).mesh is ArrayMesh: if _convert(skel, mi as MeshInstance3D): converted += 1 return converted static func _find(n: Node, klass: String, acc: Array) -> Array: if n.is_class(klass): acc.append(n) for c: Node in n.get_children(): _find(c, klass, acc) return acc static func _convert(skel: Skeleton3D, mi: MeshInstance3D) -> bool: var mesh := mi.mesh as ArrayMesh var skin := mi.skin # bind index -> (bone index, bind pose = mesh-space -> bone-local) var bind_bone: PackedInt32Array = [] var bind_pose: Array[Transform3D] = [] for b: int in skin.get_bind_count(): var bone := skin.get_bind_bone(b) if bone < 0: bone = skel.find_bone(skin.get_bind_name(b)) bind_bone.append(bone) bind_pose.append(skin.get_bind_pose(b)) # Accumulate SurfaceTool geometry per destination bone. var st_by_bone: Dictionary = {} # bone index -> SurfaceTool var mat_by_bone: Dictionary = {} # bone index -> Material (first seen) for s: int in mesh.get_surface_count(): var arr := mesh.surface_get_arrays(s) var verts: PackedVector3Array = arr[Mesh.ARRAY_VERTEX] var norms: PackedVector3Array = arr[Mesh.ARRAY_NORMAL] var uvs: PackedVector2Array = arr[Mesh.ARRAY_TEX_UV] if arr[Mesh.ARRAY_TEX_UV] != null else PackedVector2Array() var cols: PackedColorArray = arr[Mesh.ARRAY_COLOR] if arr[Mesh.ARRAY_COLOR] != null else PackedColorArray() var bones: PackedInt32Array = arr[Mesh.ARRAY_BONES] var weights: PackedFloat32Array = arr[Mesh.ARRAY_WEIGHTS] var idx: PackedInt32Array = arr[Mesh.ARRAY_INDEX] var mat := mesh.surface_get_material(s) var infl := 8 if (mesh.surface_get_format(s) & Mesh.ARRAY_FLAG_USE_8_BONE_WEIGHTS) else 4 var tri := PackedInt32Array() if idx != null and idx.size() > 0: tri = idx else: tri.resize(verts.size()) for i: int in verts.size(): tri[i] = i for t: int in range(0, tri.size(), 3): var a := tri[t] var b := tri[t + 1] var c := tri[t + 2] var bind := _dominant_bind([a, b, c], bones, weights, infl) var bone := bind_bone[bind] var pose := bind_pose[bind] var st: SurfaceTool = st_by_bone.get(bone) if st == null: st = SurfaceTool.new() st.begin(Mesh.PRIMITIVE_TRIANGLES) st_by_bone[bone] = st mat_by_bone[bone] = mat for v: int in [a, b, c]: if cols.size() > v: st.set_color(cols[v]) if uvs.size() > v: st.set_uv(uvs[v]) if norms.size() > v: st.set_normal((pose.basis * norms[v]).normalized()) st.add_vertex(pose * verts[v]) if st_by_bone.is_empty(): return false for bone: int in st_by_bone: var att := BoneAttachment3D.new() att.bone_name = skel.get_bone_name(bone) skel.add_child(att) var piece := MeshInstance3D.new() var st: SurfaceTool = st_by_bone[bone] var m := st.commit() if mat_by_bone[bone] != null: m.surface_set_material(0, mat_by_bone[bone]) piece.mesh = m att.add_child(piece) # Hide (don't free) the original: keeps the node for any gameplay code that references # it, and a hidden skinned mesh isn't drawn so it never hits the transform-feedback path. mi.visible = false return true static func _dominant_bind(vs: Array, bones: PackedInt32Array, weights: PackedFloat32Array, infl: int) -> int: var acc: Dictionary = {} for v: int in vs: for k: int in infl: var w := weights[v * infl + k] if w <= 0.0: continue var bind := bones[v * infl + k] acc[bind] = float(acc.get(bind, 0.0)) + w var best := 0 var best_w := -1.0 for bind: int in acc: if acc[bind] > best_w: best_w = acc[bind] best = bind return best