Avatar: fix inside-out black shell + add a procedural walk cycle
checkpoint / checkpoint (push) Successful in 1m52s
checkpoint / checkpoint (push) Successful in 1m52s
The blocky body rendered fully black from every angle: _append_box wound its faces clockwise-from-outside, so every computed normal pointed inward -- an inside-out shell, lit from behind on all sides, and the level had ambient light disabled so nothing lifted it. Fixed the winding (normals now verify outward 234:36) and switched test_level's environment to a soft colour ambient so shaded sides read grey, not pure black. Limbs were static because the rig had no animation at all. Added scripts/avatar/limb_animator.gd: a clip-free walk cycle that swings the upper arm/leg bones with ground speed, folds the knees on the rear stride, and eases back to rest when idle. Wired into the local player (speed from velocity) and remote players in game.gd (speed from the per-snapshot authoritative position delta). Verified: avatar smoke 14/14 from source and exported pack; fresh-clone boot PASS; game + menu boot headless clean. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015jtxAiu5dAW4WxCNdGkrkJ
This commit is contained in:
Binary file not shown.
@@ -47,9 +47,9 @@ subdivisions_height = 8
|
|||||||
[sub_resource type="Environment" id="WorldEnvironment_env"]
|
[sub_resource type="Environment" id="WorldEnvironment_env"]
|
||||||
background_mode = 1
|
background_mode = 1
|
||||||
background_color = Color(0.4, 0.6, 0.8, 1)
|
background_color = Color(0.4, 0.6, 0.8, 1)
|
||||||
ambient_light_source = 1
|
ambient_light_source = 2
|
||||||
ambient_light_energy = 0.4
|
ambient_light_color = Color(0.55, 0.6, 0.68, 1)
|
||||||
ambient_light_sky_energy = 1.0
|
ambient_light_energy = 0.5
|
||||||
|
|
||||||
[node name="TestLevel" type="Node3D"]
|
[node name="TestLevel" type="Node3D"]
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,70 @@
|
|||||||
|
extends Node
|
||||||
|
## Procedural locomotion for the placeholder blocky avatar.
|
||||||
|
##
|
||||||
|
## There are no animation clips yet -- this just swings the upper arm/leg bones
|
||||||
|
## around their local X by a sine wave whose phase advances with distance
|
||||||
|
## travelled, bends the knees on the rear half of the stride, and eases the
|
||||||
|
## whole thing back to the rest pose when the character stops.
|
||||||
|
##
|
||||||
|
## The owner sets `speed` (horizontal m/s) every frame; setup() is given the
|
||||||
|
## avatar's Skeleton3D. Bones are addressed by canonical name, so this works
|
||||||
|
## for the local player and for snapshot-driven remote players alike.
|
||||||
|
|
||||||
|
const STRIDE := 0.7 ## peak hip/shoulder swing, radians
|
||||||
|
const KNEE := 1.0 ## peak knee bend, radians
|
||||||
|
const CYCLE := 1.9 ## phase (radians) advanced per metre travelled
|
||||||
|
const BLEND := 12.0 ## ease rate toward the target pose
|
||||||
|
const MOVE_EPS := 0.35 ## m/s below which we treat the avatar as idle
|
||||||
|
|
||||||
|
var speed: float = 0.0
|
||||||
|
|
||||||
|
var _skel: Skeleton3D = null
|
||||||
|
var _idx := {} # bone name -> skeleton index
|
||||||
|
var _phase := 0.0
|
||||||
|
var _amount := 0.0 # 0 = idle, 1 = full walk
|
||||||
|
|
||||||
|
const _BONES := [
|
||||||
|
"LeftUpperLeg", "RightUpperLeg", "LeftLowerLeg", "RightLowerLeg",
|
||||||
|
"LeftUpperArm", "RightUpperArm",
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
func setup(skeleton: Skeleton3D) -> void:
|
||||||
|
_skel = skeleton
|
||||||
|
_idx.clear()
|
||||||
|
if skeleton == null:
|
||||||
|
return
|
||||||
|
for b in _BONES:
|
||||||
|
var i := skeleton.find_bone(b)
|
||||||
|
if i != -1:
|
||||||
|
_idx[b] = i
|
||||||
|
|
||||||
|
|
||||||
|
func _process(delta: float) -> void:
|
||||||
|
if _skel == null or _idx.is_empty():
|
||||||
|
return
|
||||||
|
|
||||||
|
var moving := speed > MOVE_EPS
|
||||||
|
_amount = lerpf(_amount, 1.0 if moving else 0.0, clampf(BLEND * delta, 0.0, 1.0))
|
||||||
|
_phase += maxf(speed, 0.0) * delta * CYCLE
|
||||||
|
if _phase > TAU:
|
||||||
|
_phase -= TAU
|
||||||
|
|
||||||
|
var s := sin(_phase)
|
||||||
|
var swing := s * STRIDE * _amount
|
||||||
|
|
||||||
|
_pitch("LeftUpperLeg", swing)
|
||||||
|
_pitch("RightUpperLeg", -swing)
|
||||||
|
_pitch("LeftUpperArm", -swing)
|
||||||
|
_pitch("RightUpperArm", swing)
|
||||||
|
# Knee folds only while that leg is swinging back (negative pitch phase).
|
||||||
|
_pitch("LeftLowerLeg", maxf(0.0, -s) * KNEE * _amount)
|
||||||
|
_pitch("RightLowerLeg", maxf(0.0, s) * KNEE * _amount)
|
||||||
|
|
||||||
|
|
||||||
|
func _pitch(bone: String, angle: float) -> void:
|
||||||
|
if not _idx.has(bone):
|
||||||
|
return
|
||||||
|
var i: int = _idx[bone]
|
||||||
|
var rest_q := _skel.get_bone_rest(i).basis.get_rotation_quaternion()
|
||||||
|
_skel.set_bone_pose_rotation(i, rest_q * Quaternion(Vector3.RIGHT, angle))
|
||||||
@@ -24,6 +24,7 @@ var _net: NetworkClient = null
|
|||||||
var _networked := false
|
var _networked := false
|
||||||
var _aligned := false # snapped to the server spawn yet?
|
var _aligned := false # snapped to the server spawn yet?
|
||||||
var _remotes: Dictionary = {} # peer_id -> Node3D
|
var _remotes: Dictionary = {} # peer_id -> Node3D
|
||||||
|
var _remote_prev: Dictionary = {} # peer_id -> Vector3 (last snapshot position)
|
||||||
var _status_label: Label = null
|
var _status_label: Label = null
|
||||||
|
|
||||||
|
|
||||||
@@ -132,10 +133,19 @@ func _sync_remotes(states: Dictionary, delta: float) -> void:
|
|||||||
node.global_position = node.global_position.lerp(target, clampf(REMOTE_LERP * delta, 0.0, 1.0))
|
node.global_position = node.global_position.lerp(target, clampf(REMOTE_LERP * delta, 0.0, 1.0))
|
||||||
node.rotation.y = st.get("rotation_y", node.rotation.y)
|
node.rotation.y = st.get("rotation_y", node.rotation.y)
|
||||||
|
|
||||||
|
# Drive the remote's walk cycle from how far its authoritative
|
||||||
|
# position moved this snapshot.
|
||||||
|
var prev: Vector3 = _remote_prev.get(pid, target)
|
||||||
|
var anim := node.get_node_or_null("LimbAnim")
|
||||||
|
if anim != null and delta > 0.0:
|
||||||
|
anim.set("speed", (target - prev).length() / delta)
|
||||||
|
_remote_prev[pid] = target
|
||||||
|
|
||||||
for pid in _remotes.keys():
|
for pid in _remotes.keys():
|
||||||
if not states.has(pid):
|
if not states.has(pid):
|
||||||
_remotes[pid].queue_free()
|
_remotes[pid].queue_free()
|
||||||
_remotes.erase(pid)
|
_remotes.erase(pid)
|
||||||
|
_remote_prev.erase(pid)
|
||||||
|
|
||||||
|
|
||||||
func _make_remote(pid: int) -> Node3D:
|
func _make_remote(pid: int) -> Node3D:
|
||||||
@@ -157,6 +167,12 @@ func _make_remote(pid: int) -> Node3D:
|
|||||||
m.albedo_color = tint
|
m.albedo_color = tint
|
||||||
mi.material_override = m
|
mi.material_override = m
|
||||||
root.add_child(visual)
|
root.add_child(visual)
|
||||||
|
var skel := visual.find_child("Skeleton3D", true, false)
|
||||||
|
if skel is Skeleton3D:
|
||||||
|
var anim := preload("res://scripts/avatar/limb_animator.gd").new()
|
||||||
|
anim.name = "LimbAnim"
|
||||||
|
root.add_child(anim)
|
||||||
|
anim.call("setup", skel)
|
||||||
else:
|
else:
|
||||||
var body := MeshInstance3D.new()
|
var body := MeshInstance3D.new()
|
||||||
var capsule := CapsuleMesh.new()
|
var capsule := CapsuleMesh.new()
|
||||||
|
|||||||
@@ -42,6 +42,7 @@ const PITCH_MAX: float = 0.45
|
|||||||
@onready var _capsule_mesh: MeshInstance3D = $CapsuleMesh
|
@onready var _capsule_mesh: MeshInstance3D = $CapsuleMesh
|
||||||
|
|
||||||
var _avatar_view: Node3D = null
|
var _avatar_view: Node3D = null
|
||||||
|
var _limb_anim: Node = null
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
@@ -107,6 +108,14 @@ func _apply_avatar() -> void:
|
|||||||
if _capsule_mesh:
|
if _capsule_mesh:
|
||||||
_capsule_mesh.visible = false
|
_capsule_mesh.visible = false
|
||||||
|
|
||||||
|
# Procedural walk cycle -- no clips yet, just swing the limb bones with
|
||||||
|
# ground speed.
|
||||||
|
var skel := avatar.find_child("Skeleton3D", true, false)
|
||||||
|
if skel is Skeleton3D:
|
||||||
|
_limb_anim = preload("res://scripts/avatar/limb_animator.gd").new()
|
||||||
|
add_child(_limb_anim)
|
||||||
|
_limb_anim.call("setup", skel)
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# _unhandled_input — capture keyboard / mouse for movement
|
# _unhandled_input — capture keyboard / mouse for movement
|
||||||
@@ -194,6 +203,9 @@ func _physics_process(delta: float) -> void:
|
|||||||
|
|
||||||
move_and_slide()
|
move_and_slide()
|
||||||
|
|
||||||
|
if _limb_anim != null:
|
||||||
|
_limb_anim.set("speed", Vector2(velocity.x, velocity.z).length())
|
||||||
|
|
||||||
# Safety net -- never fall forever if something goes wrong.
|
# Safety net -- never fall forever if something goes wrong.
|
||||||
if global_position.y < -10.0:
|
if global_position.y < -10.0:
|
||||||
global_position = _spawn_position
|
global_position = _spawn_position
|
||||||
|
|||||||
@@ -211,13 +211,16 @@ func _append_box(verts: PackedVector3Array, normals: PackedVector3Array,
|
|||||||
centre + Vector3(-h.x, h.y, -h.z), centre + Vector3(h.x, h.y, -h.z),
|
centre + Vector3(-h.x, h.y, -h.z), centre + Vector3(h.x, h.y, -h.z),
|
||||||
centre + Vector3(h.x, h.y, h.z), centre + Vector3(-h.x, h.y, h.z),
|
centre + Vector3(h.x, h.y, h.z), centre + Vector3(-h.x, h.y, h.z),
|
||||||
]
|
]
|
||||||
|
# Vertex order per face is CCW seen from outside -> the computed normal
|
||||||
|
# points outward. (Getting this backwards renders the whole mesh as an
|
||||||
|
# inside-out black shell: every face is lit from behind.)
|
||||||
var faces := [
|
var faces := [
|
||||||
[0, 3, 2, 1], # bottom (-Y)
|
[3, 2, 6, 7], # +Z front
|
||||||
[4, 5, 6, 7], # top (+Y)
|
[1, 0, 4, 5], # -Z back
|
||||||
[0, 1, 5, 4], # -Z
|
[7, 6, 5, 4], # +Y top
|
||||||
[2, 3, 7, 6], # +Z
|
[0, 1, 2, 3], # -Y bottom
|
||||||
[1, 2, 6, 5], # +X
|
[2, 1, 5, 6], # +X right
|
||||||
[3, 0, 4, 7], # -X
|
[3, 7, 4, 0], # -X left
|
||||||
]
|
]
|
||||||
for f in faces:
|
for f in faces:
|
||||||
var q: Array[Vector3] = [c[f[0]], c[f[1]], c[f[2]], c[f[3]]]
|
var q: Array[Vector3] = [c[f[0]], c[f[1]], c[f[2]], c[f[3]]]
|
||||||
|
|||||||
Reference in New Issue
Block a user