feat: thinner cel outline + procedural model actually holds its weapon
Outline: the inverted-hull width was tuned when the look was heavier and now reads as a thick marker stroke. Cut every call site to roughly a third (characters 0.012 -> 0.004, level geometry 0.015 -> 0.005, Neon Alley's size-scaled clamp 0.02..0.05 -> 0.007..0.018) plus the shader default, so the ink line stays readable without fattening every silhouette. Third-person weapon on HumanoidModel: the gun was parented to root_pivot at a fixed (-0.15, 1.0, 0.4) — floating 40 cm off the chest while both arms posed at it from a distance. Give the rig a real right-hand node at the end of the forearm and hang the weapon there, so it travels with the arm. Its aim is re-derived from the body each frame (hand_r_pivot's global basis is copied from root_pivot), otherwise the barrel would swing around with the arm animation. The hold pose is retuned to match: right elbow tucked at the ribs with a level forearm, left arm crossing to the handguard. _seat_weapon_in_hand() slides each weapon along its own barrel axis so a plausible grip lands in the fist regardless of where the artist left the model origin. It measures with MeshInstance3D.get_aabb(), not mesh.get_aabb() — the FBX gun parts are skinned, so the Mesh resource still carries bind-pose bounds tens of metres across. Co-Authored-By: Claude Opus 4.8 <[email protected]>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
9f60982416
commit
af8c9831c9
@@ -19,6 +19,7 @@ var upper_arm_r_pivot: Node3D
|
||||
var upper_arm_r: MeshInstance3D
|
||||
var lower_arm_r_pivot: Node3D
|
||||
var lower_arm_r: MeshInstance3D
|
||||
var hand_r_pivot: Node3D
|
||||
|
||||
var thigh_l_pivot: Node3D
|
||||
var thigh_l: MeshInstance3D
|
||||
@@ -116,7 +117,14 @@ func _ready() -> void:
|
||||
lower_arm_r.position = Vector3(0, -0.175, 0)
|
||||
lower_arm_r.cast_shadow = shadow_setting
|
||||
lower_arm_r_pivot.add_child(lower_arm_r)
|
||||
|
||||
|
||||
# Right hand: the wrist joint at the far end of the forearm. The
|
||||
# third-person weapon hangs here so it travels with the arm instead of
|
||||
# floating in front of the chest.
|
||||
hand_r_pivot = Node3D.new()
|
||||
hand_r_pivot.position = Vector3(0, -0.35, 0)
|
||||
lower_arm_r_pivot.add_child(hand_r_pivot)
|
||||
|
||||
# Legs (Thigh: 0.45 length, Calf: 0.45 length)
|
||||
var t_leg_mesh = BoxMesh.new()
|
||||
t_leg_mesh.size = Vector3(0.15, 0.45, 0.15)
|
||||
@@ -166,7 +174,7 @@ func _ready() -> void:
|
||||
# Cel-shade + ink outline the blocky fallback body so it matches skinned
|
||||
# player models (shadows-only bodies skip it: outlines would cast shadows)
|
||||
if not shadows_only:
|
||||
LevelMaterials.apply_toon_recursive(root_pivot, 0.012)
|
||||
LevelMaterials.apply_toon_recursive(root_pivot, 0.004)
|
||||
|
||||
## Skin System
|
||||
var current_skin: PlayerSkin
|
||||
@@ -229,7 +237,7 @@ func _apply_color(col: Color) -> void:
|
||||
head.mesh.material = hmat
|
||||
# Re-toon after the material swap (fresh StandardMaterials above)
|
||||
if not shadows_only:
|
||||
LevelMaterials.apply_toon_recursive(root_pivot, 0.012)
|
||||
LevelMaterials.apply_toon_recursive(root_pivot, 0.004)
|
||||
|
||||
func update_state(state: String, speed: float, is_crouching: bool = false) -> void:
|
||||
current_state = state
|
||||
@@ -240,11 +248,14 @@ func update_state(state: String, speed: float, is_crouching: bool = false) -> vo
|
||||
var is_holding_weapon: bool = false
|
||||
|
||||
func set_weapon(script_path: String) -> void:
|
||||
# Clear existing weapons from root_pivot
|
||||
for child in root_pivot.get_children():
|
||||
if child.has_meta("is_third_person_weapon"):
|
||||
child.queue_free()
|
||||
|
||||
# Clear existing weapons (older builds parented them to root_pivot)
|
||||
for parent in [root_pivot, hand_r_pivot]:
|
||||
if not parent:
|
||||
continue
|
||||
for child in parent.get_children():
|
||||
if child.has_meta("is_third_person_weapon"):
|
||||
child.queue_free()
|
||||
|
||||
if script_path == "":
|
||||
is_holding_weapon = false
|
||||
return
|
||||
@@ -269,13 +280,40 @@ func set_weapon(script_path: String) -> void:
|
||||
# Toon shading only — FBX weapon normals tear inverted-hull outlines
|
||||
LevelMaterials.apply_toon_recursive(w, 0.0)
|
||||
|
||||
# Force position after the weapon's _build_model() sets it for 1st person
|
||||
w.position = Vector3(-0.15, 1.0, 0.4)
|
||||
w.rotation_degrees = Vector3(0, 180, 0)
|
||||
# Undo the first-person viewmodel placement from _build_model()
|
||||
_seat_weapon_in_hand(w)
|
||||
)
|
||||
|
||||
# Attach to root_pivot so it stays steady and points perfectly forward
|
||||
root_pivot.add_child(w)
|
||||
|
||||
# Riding the hand keeps the gun with the arm; _process re-aims it forward.
|
||||
(hand_r_pivot if hand_r_pivot else root_pivot).add_child(w)
|
||||
|
||||
|
||||
## Slide the weapon along its own barrel axis so a plausible grip point — not
|
||||
## whatever origin the artist left the model at — ends up in the fist, and turn
|
||||
## it to face the way the body faces (the model is yawed 180° in _ready, so the
|
||||
## gun's -Z muzzle axis has to point along the model's local +Z).
|
||||
func _seat_weapon_in_hand(w: Node3D) -> void:
|
||||
w.rotation_degrees = Vector3(0, 180, 0)
|
||||
w.position = Vector3.ZERO
|
||||
var min_t := INF # most negative along the barrel = stock end
|
||||
var max_t := -INF # most positive = muzzle end
|
||||
for mi in w.find_children("*", "MeshInstance3D", true, false):
|
||||
if not mi.mesh:
|
||||
continue
|
||||
# mi.get_aabb(), NOT mi.mesh.get_aabb(): the FBX gun parts are skinned and
|
||||
# the Mesh resource still carries huge bind-pose bounds.
|
||||
var xf: Transform3D = w.global_transform.affine_inverse() * mi.global_transform
|
||||
var aabb: AABB = mi.get_aabb()
|
||||
for i in 8:
|
||||
var t: float = (xf * aabb.get_endpoint(i)).dot(Vector3(0, 0, -1))
|
||||
min_t = minf(min_t, t)
|
||||
max_t = maxf(max_t, t)
|
||||
if min_t > max_t:
|
||||
return
|
||||
# ~a third back from the muzzle is where a pistol grip sits on every gun in
|
||||
# the set, which leaves real length of weapon behind the hand.
|
||||
var grip_at := min_t + (max_t - min_t) * 0.32
|
||||
w.position = Vector3(0, 0, -grip_at)
|
||||
|
||||
func _set_shadows_recursive(node: Node) -> void:
|
||||
if node is GeometryInstance3D:
|
||||
@@ -438,15 +476,18 @@ func _process(delta: float) -> void:
|
||||
|
||||
# Override right arm (and left) if holding a weapon
|
||||
if is_holding_weapon and current_state != "death":
|
||||
# Pose the right arm to look like it's holding the weapon handle
|
||||
t_upper_arm_r_rot.x = -0.6
|
||||
t_upper_arm_r_rot.z = 0.15
|
||||
t_lower_arm_r_rot.x = -1.0
|
||||
|
||||
# Pose the left arm to look like it's holding the foregrip
|
||||
t_upper_arm_l_rot.x = -0.5
|
||||
t_upper_arm_l_rot.z = -0.15
|
||||
t_lower_arm_l_rot.x = -1.1
|
||||
# Right arm holds the grip: elbow tucked at the ribs, forearm level so
|
||||
# the fist (and the weapon parented to it) sits chest-high just in
|
||||
# front of the torso rather than out at arm's length.
|
||||
t_upper_arm_r_rot.x = -0.15
|
||||
t_upper_arm_r_rot.z = -0.10
|
||||
t_lower_arm_r_rot.x = -1.45
|
||||
|
||||
# Left arm crosses over to the handguard: up, in, and a little further
|
||||
# forward than the grip hand.
|
||||
t_upper_arm_l_rot.x = -0.55
|
||||
t_upper_arm_l_rot.z = 0.55
|
||||
t_lower_arm_l_rot.x = -1.0
|
||||
|
||||
var lerp_speed = 40.0 * delta
|
||||
root_pivot.position = root_pivot.position.lerp(t_root_pos, lerp_speed)
|
||||
@@ -462,6 +503,12 @@ func _process(delta: float) -> void:
|
||||
calf_l_pivot.rotation = _lerp_vec3(calf_l_pivot.rotation, t_calf_l_rot, lerp_speed)
|
||||
calf_r_pivot.rotation = _lerp_vec3(calf_r_pivot.rotation, t_calf_r_rot, lerp_speed)
|
||||
|
||||
# The weapon rides the hand for position, but its AIM belongs to the body:
|
||||
# cancel the arm chain's rotation so the barrel stays pointed where the
|
||||
# player faces instead of swinging around with the arm animation.
|
||||
if hand_r_pivot:
|
||||
hand_r_pivot.global_basis = root_pivot.global_basis
|
||||
|
||||
func _lerp_vec3(a: Vector3, b: Vector3, t: float) -> Vector3:
|
||||
return Vector3(
|
||||
lerp_angle(a.x, b.x, t),
|
||||
|
||||
Reference in New Issue
Block a user