feat: third-person camera toggle (V) + fix first-person showing inside the model
The first-person head-shrink (SkeletonModifier3D) left the camera looking at the inside of the neck/torso/hair. Replaced it: the owner's model now renders shadows-only in first person (clean FPS view, still fully animated and visible to others and in shadows). Added a third-person toggle (V / toggle_camera_view) so you can actually see your own animated model — the easiest way to verify a new skin's animations: - over-the-shoulder SpringArm3D camera (wall-collision aware) on the local player; press V to swap, press again to return - firing still uses the first-person camera, so aim is unchanged - reveals the owner's model via set_owner_visible() on both SkinnedPlayerModel and HumanoidModel; hides the first-person weapon viewmodel while in TP - death forces first person so the toggle can't fight the death cam Smoke test extended to cover the toggle (camera boom created, becomes current, round-trips) — 28/28 checks pass for both color and GLB skins. Co-Authored-By: Claude Fable 5 <[email protected]>
This commit is contained in:
co-authored by
Claude Fable 5
parent
64bbbf93c6
commit
e2fbc424a7
@@ -9,9 +9,12 @@ class_name SkinnedPlayerModel
|
||||
## - set_weapon(script_path) — third-person weapon in hand
|
||||
## - shadows_only — legacy local-player mode
|
||||
##
|
||||
## View modes:
|
||||
## - first_person_mode = true → full body visible to the OWNER, head hidden
|
||||
## (shrunk via a SkeletonModifier3D so hair/face follow), fully animated.
|
||||
## View modes (for the LOCAL player only):
|
||||
## - first_person_mode = true → model renders shadows-only for the owner, so
|
||||
## the camera (which sits inside the head) never shows the inside of the
|
||||
## mesh. Still fully animated; still visible to other players and in shadows.
|
||||
## - Press the third-person toggle → set_owner_visible(true) makes the full
|
||||
## animated model visible to the owner too (over-the-shoulder camera).
|
||||
## - first_person_mode = false → full third-person model for other players.
|
||||
|
||||
@export var model_path: String = ""
|
||||
@@ -54,7 +57,6 @@ var loaded: bool = false
|
||||
var _resolved_clips: Dictionary = {} # canonical name -> actual clip name
|
||||
var _current_clip: String = ""
|
||||
var _weapon_attachment: BoneAttachment3D
|
||||
var _head_hider: SkeletonModifier3D
|
||||
var is_holding_weapon: bool = false
|
||||
|
||||
|
||||
@@ -91,10 +93,11 @@ func load_model(path: String) -> void:
|
||||
else:
|
||||
push_warning("SkinnedPlayerModel: no animations in '%s' — model will T-pose" % path)
|
||||
|
||||
if shadows_only:
|
||||
# The local owner renders shadows-only (the camera is inside the head, so
|
||||
# showing the mesh would show the inside of it). Other players see it fully.
|
||||
# The third-person toggle calls set_owner_visible(true) to reveal it.
|
||||
if shadows_only or first_person_mode:
|
||||
_set_shadows_recursive(self)
|
||||
if first_person_mode:
|
||||
_setup_first_person()
|
||||
|
||||
loaded = true
|
||||
_play_clip("Idle")
|
||||
@@ -132,40 +135,21 @@ func _find_clip(available: PackedStringArray, wanted: String) -> String:
|
||||
|
||||
# ── View modes ────────────────────────────────────────────────────────────────
|
||||
|
||||
func _setup_first_person() -> void:
|
||||
if not skeleton:
|
||||
return
|
||||
var head_idx := _find_bone(["Head"])
|
||||
if head_idx < 0:
|
||||
# No head bone — fall back to shadows-only so the local player at least
|
||||
# isn't staring at the inside of a face.
|
||||
_set_shadows_recursive(self)
|
||||
return
|
||||
_head_hider = HeadHider.new()
|
||||
_head_hider.name = "HeadHider"
|
||||
_head_hider.head_bone = head_idx
|
||||
skeleton.add_child(_head_hider)
|
||||
## Show or hide the model to its OWNER. In first person we render shadows-only
|
||||
## (on=false) so the camera doesn't see the inside of the mesh; the third-person
|
||||
## toggle calls this with on=true to reveal the full animated model. Either way
|
||||
## the model keeps casting shadows and stays visible to other players.
|
||||
func set_owner_visible(on: bool) -> void:
|
||||
var mode := GeometryInstance3D.SHADOW_CASTING_SETTING_ON if on \
|
||||
else GeometryInstance3D.SHADOW_CASTING_SETTING_SHADOWS_ONLY
|
||||
_set_shadow_mode_recursive(self, mode)
|
||||
|
||||
|
||||
func set_first_person(enabled: bool) -> void:
|
||||
first_person_mode = enabled
|
||||
if enabled and not _head_hider:
|
||||
_setup_first_person()
|
||||
elif not enabled and _head_hider:
|
||||
_head_hider.queue_free()
|
||||
_head_hider = null
|
||||
|
||||
|
||||
## Shrinks the head bone after each animation update so the owner's camera
|
||||
## never sees their own face/hair, while shadows and other players see the
|
||||
## full head (the modifier only runs on this local instance).
|
||||
class HeadHider extends SkeletonModifier3D:
|
||||
var head_bone: int = -1
|
||||
|
||||
func _process_modification() -> void:
|
||||
var skel := get_skeleton()
|
||||
if skel and head_bone >= 0:
|
||||
skel.set_bone_pose_scale(head_bone, Vector3(0.001, 0.001, 0.001))
|
||||
func _set_shadow_mode_recursive(node: Node, mode: int) -> void:
|
||||
if node is GeometryInstance3D:
|
||||
node.cast_shadow = mode
|
||||
for child in node.get_children():
|
||||
_set_shadow_mode_recursive(child, mode)
|
||||
|
||||
|
||||
# ── Animation state ───────────────────────────────────────────────────────────
|
||||
|
||||
Reference in New Issue
Block a user