129 lines
4.5 KiB
GDScript
129 lines
4.5 KiB
GDScript
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
|