981ebf1910
Remove tools/fstest.html scratch page used to probe browser fullscreen/orientation APIs. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EznnY8rH2dXhtono1kwsXg
152 lines
5.7 KiB
GDScript
152 lines
5.7 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, 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
|