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, source surface). Keying on the # surface too — not just the bone — is what preserves materials: a head bone that carries # both the skin and hair surfaces must stay two pieces with two materials, otherwise every # surface funnelled to a bone collapses onto one material (bald matadors, wrong uniforms). var groups: Dictionary = {} # "bone:surface" -> {"st": SurfaceTool, "mat": Material, "bone": int} 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] # Honour a per-instance override (whole mesh) or per-surface override the scene set, # falling back to the material baked into the mesh surface. var mat: Material = mi.material_override if mat == null: mat = mi.get_surface_override_material(s) if mat == null: 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 key := "%d:%d" % [bone, s] var group: Dictionary = groups.get(key, {}) if group.is_empty(): var new_st := SurfaceTool.new() new_st.begin(Mesh.PRIMITIVE_TRIANGLES) group = {"st": new_st, "mat": mat, "bone": bone} groups[key] = group var st: SurfaceTool = group["st"] 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 groups.is_empty(): return false # Combine all of a bone's surface-groups into ONE mesh under ONE BoneAttachment (each group # stays its own surface + material). Same draw calls as before, but roughly half the nodes to # transform/cull every frame — which is what a phone feels while ragdolls drive the skeleton. var by_bone: Dictionary = {} # bone:int -> Array[Dictionary] for key: String in groups: var group: Dictionary = groups[key] var bone: int = group["bone"] if not by_bone.has(bone): by_bone[bone] = [] (by_bone[bone] as Array).append(group) for bone: int in by_bone: var att := BoneAttachment3D.new() att.bone_name = skel.get_bone_name(bone) skel.add_child(att) var piece := MeshInstance3D.new() var combined := ArrayMesh.new() for group: Dictionary in by_bone[bone]: var st: SurfaceTool = group["st"] st.commit(combined) if group["mat"] != null: combined.surface_set_material(combined.get_surface_count() - 1, group["mat"]) piece.mesh = combined 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