Add blood decals, gore, mobile HUD, web start gate + touch/perf tests

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
This commit is contained in:
2026-09-04 14:07:57 +03:00
parent 01d6ecf475
commit 981ebf1910
32 changed files with 1536 additions and 113 deletions
+40 -17
View File
@@ -43,9 +43,11 @@ static func _convert(skel: Skeleton3D, mi: MeshInstance3D) -> bool:
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)
# 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)
@@ -56,7 +58,13 @@ static func _convert(skel: Skeleton3D, mi: MeshInstance3D) -> bool:
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)
# 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()
@@ -74,12 +82,14 @@ static func _convert(skel: Skeleton3D, mi: MeshInstance3D) -> bool:
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
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])
@@ -89,19 +99,32 @@ static func _convert(skel: Skeleton3D, mi: MeshInstance3D) -> bool:
st.set_normal((pose.basis * norms[v]).normalized())
st.add_vertex(pose * verts[v])
if st_by_bone.is_empty():
if groups.is_empty():
return false
for bone: int in st_by_bone:
# 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 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
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