Feat/outline thickness and tp weapon hold #22
@@ -1,153 +0,0 @@
|
|||||||
extends Object
|
|
||||||
class_name SkinMeshRepair
|
|
||||||
|
|
||||||
## Removes below-the-knee geometry that is welded across BOTH legs.
|
|
||||||
##
|
|
||||||
## Taila's ClothCAndW surface carries a ~10 cm strip at ankle height (spanning
|
|
||||||
## x = -0.05 .. +0.05, straight across the centre line) whose vertices are
|
|
||||||
## weighted to the left leg on one edge and the right leg on the other. Nothing
|
|
||||||
## about the rig or the clips is wrong — the foot bones swing independently
|
|
||||||
## (measured left/right correlation -0.94) and no bone is ever scaled. But a
|
|
||||||
## triangle with one corner on each leg has no correct pose: the moment the legs
|
|
||||||
## separate it MUST stretch. It read as the two ankle cuffs being welded
|
|
||||||
## together.
|
|
||||||
##
|
|
||||||
## An earlier version of this pass tried to re-weight those vertices to the
|
|
||||||
## nearer leg. That was wrong: the strip is real geometry spanning the gap, so
|
|
||||||
## re-weighting only tore it in half — a visible seam that still stretched.
|
|
||||||
## Deleting the cross-leg triangles is the only stable answer, and it is safe
|
|
||||||
## because such a triangle is never legitimate below the knee.
|
|
||||||
##
|
|
||||||
## ABOVE the knee it very much is legitimate — the shorts and skirt genuinely
|
|
||||||
## span from left-thigh to right-thigh weights at the crotch — so the cut is
|
|
||||||
## limited to below the knee, taken from the skeleton's own rest pose rather
|
|
||||||
## than a hardcoded height, so it holds for any character's proportions.
|
|
||||||
|
|
||||||
const LEG_BONE_HINTS := ["thigh", "shin", "foot", "toe"]
|
|
||||||
|
|
||||||
|
|
||||||
## Returns the number of triangles removed, so callers can log whether a skin
|
|
||||||
## needed the repair at all.
|
|
||||||
static func repair(root: Node, skeleton: Skeleton3D) -> int:
|
|
||||||
var knee_y := _knee_height(skeleton)
|
|
||||||
if is_nan(knee_y):
|
|
||||||
return 0
|
|
||||||
var removed_total := 0
|
|
||||||
for mi in root.find_children("*", "MeshInstance3D", true, false):
|
|
||||||
if mi.mesh == null or mi.skin == null:
|
|
||||||
continue
|
|
||||||
# Rebuilding a mesh drops blend shapes, so skins that use them (face
|
|
||||||
# rigs) are left alone rather than silently losing their expressions.
|
|
||||||
if mi.mesh.get_blend_shape_count() > 0:
|
|
||||||
continue
|
|
||||||
removed_total += _repair_mesh(mi, skeleton, knee_y)
|
|
||||||
return removed_total
|
|
||||||
|
|
||||||
|
|
||||||
## Knee height in skeleton space — the boundary above which cross-leg geometry
|
|
||||||
## is legitimate.
|
|
||||||
static func _knee_height(skeleton: Skeleton3D) -> float:
|
|
||||||
if skeleton == null:
|
|
||||||
return NAN
|
|
||||||
var best := NAN
|
|
||||||
for name in ["DEF-shin.L", "shin.L", "DEF-shin.R", "shin.R"]:
|
|
||||||
var i := skeleton.find_bone(name)
|
|
||||||
if i >= 0:
|
|
||||||
best = skeleton.get_bone_global_rest(i).origin.y
|
|
||||||
break
|
|
||||||
return best
|
|
||||||
|
|
||||||
|
|
||||||
static func _repair_mesh(mi: MeshInstance3D, skeleton: Skeleton3D,
|
|
||||||
knee_y: float) -> int:
|
|
||||||
var side := _side_map(mi.skin, skeleton)
|
|
||||||
var surfaces: Array = []
|
|
||||||
var removed_total := 0
|
|
||||||
for s in range(mi.mesh.get_surface_count()):
|
|
||||||
var arrays: Array = mi.mesh.surface_get_arrays(s)
|
|
||||||
removed_total += _repair_surface(arrays, side, knee_y)
|
|
||||||
surfaces.append({
|
|
||||||
"arrays": arrays,
|
|
||||||
"material": mi.mesh.surface_get_material(s),
|
|
||||||
"name": mi.mesh.surface_get_name(s),
|
|
||||||
})
|
|
||||||
if removed_total == 0:
|
|
||||||
return 0
|
|
||||||
|
|
||||||
var rebuilt := ArrayMesh.new()
|
|
||||||
for i in surfaces.size():
|
|
||||||
var entry: Dictionary = surfaces[i]
|
|
||||||
rebuilt.add_surface_from_arrays(Mesh.PRIMITIVE_TRIANGLES, entry["arrays"])
|
|
||||||
rebuilt.surface_set_material(i, entry["material"])
|
|
||||||
if entry["name"] != "":
|
|
||||||
rebuilt.surface_set_name(i, entry["name"])
|
|
||||||
mi.mesh = rebuilt
|
|
||||||
return removed_total
|
|
||||||
|
|
||||||
|
|
||||||
## -1 left leg, +1 right leg, 0 anything else — keyed by SKIN BIND index, which
|
|
||||||
## is what ARRAY_BONES stores (not the skeleton's bone index).
|
|
||||||
static func _side_map(skin: Skin, skeleton: Skeleton3D) -> PackedInt32Array:
|
|
||||||
var out := PackedInt32Array()
|
|
||||||
out.resize(skin.get_bind_count())
|
|
||||||
for b in skin.get_bind_count():
|
|
||||||
var n := skin.get_bind_name(b)
|
|
||||||
if n == "":
|
|
||||||
var bone := skin.get_bind_bone(b)
|
|
||||||
n = skeleton.get_bone_name(bone) if bone >= 0 else ""
|
|
||||||
var is_leg := false
|
|
||||||
for hint in LEG_BONE_HINTS:
|
|
||||||
if n.findn(hint) != -1:
|
|
||||||
is_leg = true
|
|
||||||
break
|
|
||||||
if not is_leg:
|
|
||||||
out[b] = 0
|
|
||||||
elif n.ends_with(".L"):
|
|
||||||
out[b] = -1
|
|
||||||
elif n.ends_with(".R"):
|
|
||||||
out[b] = 1
|
|
||||||
else:
|
|
||||||
out[b] = 0
|
|
||||||
return out
|
|
||||||
|
|
||||||
|
|
||||||
static func _repair_surface(arrays: Array, side: PackedInt32Array,
|
|
||||||
knee_y: float) -> int:
|
|
||||||
var verts: PackedVector3Array = arrays[Mesh.ARRAY_VERTEX]
|
|
||||||
var bones: PackedInt32Array = arrays[Mesh.ARRAY_BONES]
|
|
||||||
var idx: PackedInt32Array = arrays[Mesh.ARRAY_INDEX]
|
|
||||||
var weights: PackedFloat32Array = arrays[Mesh.ARRAY_WEIGHTS]
|
|
||||||
if bones.is_empty() or idx.is_empty() or verts.is_empty():
|
|
||||||
return 0
|
|
||||||
var per: int = bones.size() / verts.size()
|
|
||||||
|
|
||||||
var vside := PackedInt32Array()
|
|
||||||
vside.resize(verts.size())
|
|
||||||
for v in verts.size():
|
|
||||||
var best_w := 0.0
|
|
||||||
var best_s := 0
|
|
||||||
for k in per:
|
|
||||||
var w: float = weights[v * per + k]
|
|
||||||
if w > best_w:
|
|
||||||
best_w = w
|
|
||||||
best_s = side[bones[v * per + k]]
|
|
||||||
vside[v] = best_s
|
|
||||||
|
|
||||||
var keep := PackedInt32Array()
|
|
||||||
var removed := 0
|
|
||||||
for t in range(0, idx.size(), 3):
|
|
||||||
var a: int = idx[t]
|
|
||||||
var b: int = idx[t + 1]
|
|
||||||
var c: int = idx[t + 2]
|
|
||||||
var below_knee: bool = (verts[a].y + verts[b].y + verts[c].y) / 3.0 < knee_y
|
|
||||||
var has_l: bool = vside[a] == -1 or vside[b] == -1 or vside[c] == -1
|
|
||||||
var has_r: bool = vside[a] == 1 or vside[b] == 1 or vside[c] == 1
|
|
||||||
if below_knee and has_l and has_r:
|
|
||||||
removed += 1
|
|
||||||
continue
|
|
||||||
keep.append(a)
|
|
||||||
keep.append(b)
|
|
||||||
keep.append(c)
|
|
||||||
if removed > 0:
|
|
||||||
arrays[Mesh.ARRAY_INDEX] = keep
|
|
||||||
return removed
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
uid://bak2pood5a40t
|
|
||||||
@@ -56,18 +56,18 @@ const LOOPING_CLIPS := ["Idle", "Walk", "Run", "Sprint", "Fall", "Crouch",
|
|||||||
"CrouchIdle", "CrouchWalk", "Slide", "WallRun", "WallCling", "Grapple",
|
"CrouchIdle", "CrouchWalk", "Slide", "WallRun", "WallCling", "Grapple",
|
||||||
"Dance", "PistolIdle"]
|
"Dance", "PistolIdle"]
|
||||||
|
|
||||||
const BLEND_TIME := 0.22
|
const BLEND_TIME := 0.32
|
||||||
## Per-clip blend overrides. Reaction moves still need to read as instant, but
|
## Per-clip blend overrides. Reaction moves still need to read as instant, but
|
||||||
## nothing cuts hard any more — every clip cross-fades. Locomotion gets the
|
## nothing cuts hard any more — every clip cross-fades. Locomotion gets the
|
||||||
## longest fades because Idle<->Walk<->Run<->Sprint switch constantly as speed
|
## longest fades because Idle<->Walk<->Run<->Sprint switch constantly as speed
|
||||||
## drifts across their thresholds, and that is where hard cuts were most
|
## drifts across their thresholds, and that is where hard cuts were most
|
||||||
## visible.
|
## visible.
|
||||||
const BLEND_TIMES := {
|
const BLEND_TIMES := {
|
||||||
"Dash": 0.10, "Jump": 0.12, "Hit": 0.08, "Land": 0.12,
|
"Dash": 0.14, "Jump": 0.16, "Hit": 0.10, "Land": 0.16,
|
||||||
"Slide": 0.16, "Death": 0.15, "Throw": 0.10, "PistolReload": 0.18,
|
"Slide": 0.22, "Death": 0.20, "Throw": 0.14, "PistolReload": 0.24,
|
||||||
"Idle": 0.30, "PistolIdle": 0.30, "Walk": 0.28, "Run": 0.28, "Sprint": 0.28,
|
"Idle": 0.42, "PistolIdle": 0.42, "Walk": 0.40, "Run": 0.40, "Sprint": 0.40,
|
||||||
"CrouchIdle": 0.28, "CrouchWalk": 0.28, "Fall": 0.20, "WallRun": 0.22,
|
"CrouchIdle": 0.40, "CrouchWalk": 0.40, "Fall": 0.28, "WallRun": 0.30,
|
||||||
"WallCling": 0.20, "Grapple": 0.20,
|
"WallCling": 0.28, "Grapple": 0.28,
|
||||||
}
|
}
|
||||||
|
|
||||||
## Named gameplay actions -> (clip, lock seconds). Networked via the
|
## Named gameplay actions -> (clip, lock seconds). Networked via the
|
||||||
@@ -162,13 +162,6 @@ func load_model(path: String) -> void:
|
|||||||
push_warning("SkinnedPlayerModel: no skeleton in '%s'" % path)
|
push_warning("SkinnedPlayerModel: no skeleton in '%s'" % path)
|
||||||
else:
|
else:
|
||||||
_ensure_meshes_bound(scene)
|
_ensure_meshes_bound(scene)
|
||||||
# Below-the-knee geometry welded across both legs can only ever stretch
|
|
||||||
# as they separate — it made the ankle cuffs look linked. See
|
|
||||||
# SkinMeshRepair.
|
|
||||||
var culled := SkinMeshRepair.repair(scene, skeleton)
|
|
||||||
if culled > 0:
|
|
||||||
print("SkinnedPlayerModel: removed %d cross-leg triangles from '%s'"
|
|
||||||
% [culled, path.get_file()])
|
|
||||||
_pose_mod = ShooterPoseModifier.new()
|
_pose_mod = ShooterPoseModifier.new()
|
||||||
_pose_mod.name = "ShooterPose"
|
_pose_mod.name = "ShooterPose"
|
||||||
skeleton.add_child(_pose_mod)
|
skeleton.add_child(_pose_mod)
|
||||||
@@ -438,11 +431,19 @@ func update_state(state: String, speed: float, is_crouching: bool = false) -> vo
|
|||||||
# Scale locomotion playback so feet keep up with actual movement speed.
|
# Scale locomotion playback so feet keep up with actual movement speed.
|
||||||
if _anim_tree:
|
if _anim_tree:
|
||||||
var s := 1.0
|
var s := 1.0
|
||||||
|
var is_loco := false
|
||||||
match clip:
|
match clip:
|
||||||
"Walk", "CrouchWalk":
|
"Walk", "CrouchWalk":
|
||||||
s = clampf(speed / walk_anim_reference_speed, 0.7, 1.6)
|
s = clampf(speed / walk_anim_reference_speed, 0.7, 1.6)
|
||||||
|
is_loco = true
|
||||||
"Run", "Sprint", "WallRun":
|
"Run", "Sprint", "WallRun":
|
||||||
s = clampf(speed / run_anim_reference_speed, 0.7, 1.8)
|
s = clampf(speed / run_anim_reference_speed, 0.7, 1.8)
|
||||||
|
is_loco = true
|
||||||
|
# Backpedalling: run the cycle BACKWARDS rather than moon-walking with
|
||||||
|
# the forward clip. The shared library has no authored reverse run, and
|
||||||
|
# a reversed stride reads correctly for a backpedal.
|
||||||
|
if is_loco and _cur_fwd < -0.25:
|
||||||
|
s = -s
|
||||||
_anim_tree.set("parameters/loco_scale/scale", s)
|
_anim_tree.set("parameters/loco_scale/scale", s)
|
||||||
|
|
||||||
|
|
||||||
@@ -743,8 +744,11 @@ class ShooterPoseModifier extends SkeletonModifier3D:
|
|||||||
var reload_phase: float = 0.0
|
var reload_phase: float = 0.0
|
||||||
|
|
||||||
# Tuning (radians). Positive pitch leans forward; positive roll leans right.
|
# Tuning (radians). Positive pitch leans forward; positive roll leans right.
|
||||||
const LEAN_ROLL := 0.30
|
# The lean is the ONLY thing that tells a viewer which way this character is
|
||||||
const LEAN_PITCH := 0.18
|
# travelling — the library has one forward locomotion cycle and no strafe
|
||||||
|
# clips — so it has to be legible, not subtle.
|
||||||
|
const LEAN_ROLL := 0.42
|
||||||
|
const LEAN_PITCH := 0.30
|
||||||
const SLIDE_BACK := 0.75 # torso lean-back during slide
|
const SLIDE_BACK := 0.75 # torso lean-back during slide
|
||||||
const SLIDE_HEAD_UP := 0.7 # head pitch to keep looking forward
|
const SLIDE_HEAD_UP := 0.7 # head pitch to keep looking forward
|
||||||
const SLIDE_LEG_FWD := 0.95 # thighs swing forward so feet lead the slide
|
const SLIDE_LEG_FWD := 0.95 # thighs swing forward so feet lead the slide
|
||||||
|
|||||||
@@ -102,13 +102,23 @@ const CHARACTER_INK := Color(0.07, 0.06, 0.09)
|
|||||||
## Second pass for IMPORTED CHARACTER models (the anime GLB skins), run right
|
## Second pass for IMPORTED CHARACTER models (the anime GLB skins), run right
|
||||||
## after apply_toon_recursive. Two things those models need that props don't:
|
## after apply_toon_recursive. Two things those models need that props don't:
|
||||||
##
|
##
|
||||||
## 1. Their line-work is part of the mesh. Anime GLBs ship an inverted-hull
|
## 1. Their line-work is part of the mesh, as extra UNTEXTURED surfaces, and it
|
||||||
## outline shell plus eye-line/eye-highlight cards as extra, UNTEXTURED
|
## splits into two kinds that need opposite treatment:
|
||||||
## surfaces (Taila names them FullBlack / EyesFullBlack / EyesInvL / EyesHL).
|
##
|
||||||
## They are authored to read as flat black — or flat white for a highlight —
|
## * The body/hair OUTLINE HULL (Taila's "FullBlack" and "material") is a
|
||||||
## and the glTF import hands them a default near-white albedo. Toon-LIGHTING
|
## duplicated shell. Its skin weights do not track the base mesh through a
|
||||||
## that turns every black shell pale: that was the thin white rim tracing
|
## deep bend, so during a run it tears into spikes and stretches sheets
|
||||||
## every hair strand.
|
## between the ankles — that is what made the ankle cuffs look welded
|
||||||
|
## together. It is also redundant: characters already get an inverted-hull
|
||||||
|
## overlay from apply_toon_recursive AND the screen-space ink_edge pass.
|
||||||
|
## So it is HIDDEN outright, which removes the artefact and the redundancy
|
||||||
|
## in one go.
|
||||||
|
##
|
||||||
|
## * The EYE cards ("EyesFullBlack" lashes, "EyesInvL", "EyesHL" highlight)
|
||||||
|
## are real facial features, not a hull, and they are kept — flat, because
|
||||||
|
## the glTF import hands every untextured surface a default near-white
|
||||||
|
## albedo and toon-LIGHTING the black ones was the thin white rim that used
|
||||||
|
## to trace every hair strand.
|
||||||
##
|
##
|
||||||
## 2. Their textures are ALREADY painted with cel shading. Stacking the hard
|
## 2. Their textures are ALREADY painted with cel shading. Stacking the hard
|
||||||
## 3-tone break on top read as gloss — a bright stripe sliding across the
|
## 3-tone break on top read as gloss — a bright stripe sliding across the
|
||||||
@@ -126,13 +136,21 @@ static func apply_character_look(root: Node) -> void:
|
|||||||
continue
|
continue
|
||||||
if src.albedo_texture == null:
|
if src.albedo_texture == null:
|
||||||
# Untextured surface on a character = the model's own line-work.
|
# Untextured surface on a character = the model's own line-work.
|
||||||
|
var name := src.resource_name.to_lower()
|
||||||
var flat := StandardMaterial3D.new()
|
var flat := StandardMaterial3D.new()
|
||||||
flat.shading_mode = BaseMaterial3D.SHADING_MODE_UNSHADED
|
flat.shading_mode = BaseMaterial3D.SHADING_MODE_UNSHADED
|
||||||
flat.cull_mode = src.cull_mode
|
flat.cull_mode = src.cull_mode
|
||||||
# "HL" marks a highlight card (the glint in the pupil) — that
|
if not name.begins_with("eyes"):
|
||||||
# one really is meant to be white.
|
# The outline HULL: hide it (see above). Fully transparent
|
||||||
var is_highlight: bool = src.resource_name.to_lower().contains("hl")
|
# rather than deleted so the surface indices, and therefore
|
||||||
flat.albedo_color = Color.WHITE if is_highlight else CHARACTER_INK
|
# the mesh's own skin bindings, stay exactly as imported.
|
||||||
|
flat.transparency = BaseMaterial3D.TRANSPARENCY_ALPHA
|
||||||
|
flat.albedo_color = Color(0, 0, 0, 0)
|
||||||
|
else:
|
||||||
|
# An eye card. "HL" marks the highlight (the glint in the
|
||||||
|
# pupil) — that one really is meant to be white.
|
||||||
|
flat.albedo_color = Color.WHITE if name.contains("hl") \
|
||||||
|
else CHARACTER_INK
|
||||||
mi.set_surface_override_material(s, flat)
|
mi.set_surface_override_material(s, flat)
|
||||||
continue
|
continue
|
||||||
var toon: ShaderMaterial = mi.get_surface_override_material(s) as ShaderMaterial
|
var toon: ShaderMaterial = mi.get_surface_override_material(s) as ShaderMaterial
|
||||||
|
|||||||
Reference in New Issue
Block a user