diff --git a/assets/avatars/accessory_belt_01.glb b/assets/avatars/accessory_belt_01.glb index 9d254b0..fb5c9f4 100644 Binary files a/assets/avatars/accessory_belt_01.glb and b/assets/avatars/accessory_belt_01.glb differ diff --git a/assets/avatars/accessory_necklace_01.glb b/assets/avatars/accessory_necklace_01.glb index cd46013..400a328 100644 Binary files a/assets/avatars/accessory_necklace_01.glb and b/assets/avatars/accessory_necklace_01.glb differ diff --git a/assets/avatars/base_human_male_01.glb b/assets/avatars/base_human_male_01.glb index 0ebe817..dd0d308 100644 Binary files a/assets/avatars/base_human_male_01.glb and b/assets/avatars/base_human_male_01.glb differ diff --git a/scenes/dev/avatar_preview.tscn b/scenes/dev/avatar_preview.tscn new file mode 100644 index 0000000..8664331 --- /dev/null +++ b/scenes/dev/avatar_preview.tscn @@ -0,0 +1,6 @@ +[gd_scene load_steps=2 format=3 uid="uid://c0avpre7dev0rig"] + +[ext_resource type="Script" path="res://scripts/tools/avatar_preview.gd" id="1"] + +[node name="AvatarPreview" type="Node3D"] +script = ExtResource("1") diff --git a/scenes/dev/screenshot_rig.tscn b/scenes/dev/screenshot_rig.tscn new file mode 100644 index 0000000..7ea02ea --- /dev/null +++ b/scenes/dev/screenshot_rig.tscn @@ -0,0 +1,6 @@ +[gd_scene load_steps=2 format=3 uid="uid://bqs8xk2rig0dev"] + +[ext_resource type="Script" path="res://scripts/tools/screenshot_rig.gd" id="1"] + +[node name="ScreenshotRig" type="Node"] +script = ExtResource("1") diff --git a/scenes/worlds/test_level.tscn b/scenes/worlds/test_level.tscn index 64f2eac..89f33a7 100644 --- a/scenes/worlds/test_level.tscn +++ b/scenes/worlds/test_level.tscn @@ -46,10 +46,10 @@ subdivisions_height = 8 [sub_resource type="Environment" id="WorldEnvironment_env"] background_mode = 1 -background_color = Color(0.4, 0.6, 0.8, 1) +background_color = Color(0.45, 0.62, 0.82, 1) ambient_light_source = 2 -ambient_light_color = Color(0.55, 0.6, 0.68, 1) -ambient_light_energy = 0.5 +ambient_light_color = Color(0.7, 0.75, 0.82, 1) +ambient_light_energy = 0.8 [node name="TestLevel" type="Node3D"] @@ -110,7 +110,8 @@ shape = SubResource("SphereShape3D_prop5") mesh = SubResource("SphereMesh_prop5") [node name="Sun" type="DirectionalLight3D" parent="."] -rotation = Vector3(-0.785398, 0, 0) +rotation = Vector3(-0.95, -0.5, 0) +light_energy = 1.3 shadow_enabled = true [node name="WorldEnvironment" type="WorldEnvironment" parent="."] diff --git a/scripts/avatar/limb_animator.gd b/scripts/avatar/limb_animator.gd index 568acd0..d58aa24 100644 --- a/scripts/avatar/limb_animator.gd +++ b/scripts/avatar/limb_animator.gd @@ -10,8 +10,9 @@ extends Node ## 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 STRIDE := 0.42 ## peak hip swing, radians (~24 deg) +const ARM_SWING := 0.32 ## peak shoulder swing, radians +const KNEE := 0.5 ## 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 @@ -52,11 +53,12 @@ func _process(delta: float) -> void: var s := sin(_phase) var swing := s * STRIDE * _amount + var arm := s * ARM_SWING * _amount _pitch("LeftUpperLeg", swing) _pitch("RightUpperLeg", -swing) - _pitch("LeftUpperArm", -swing) - _pitch("RightUpperArm", swing) + _pitch("LeftUpperArm", -arm) + _pitch("RightUpperArm", arm) # 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) diff --git a/scripts/tools/avatar_preview.gd b/scripts/tools/avatar_preview.gd new file mode 100644 index 0000000..bec5a06 --- /dev/null +++ b/scripts/tools/avatar_preview.gd @@ -0,0 +1,69 @@ +extends Node3D +## Dev-only. Builds the current avatar via AvatarSystem, stands it on a small +## stage with even lighting, and (optionally) drives the procedural walk cycle +## so screenshots can verify both the silhouette and that the limbs move. +## +## Env: +## AGP_PREVIEW_WALK "1" -> feed the limb animator a walking speed +## AGP_PREVIEW_SPIN degrees/sec turntable (default 0) +## Used together with screenshot_rig.gd (AGP_SHOT_SCENE=res://scenes/dev/avatar_preview.tscn, +## AGP_SHOT_SEQ="6:10" to catch the cycle at several phases). + +const LimbAnimator := preload("res://scripts/avatar/limb_animator.gd") + +var _avatar: Node3D = null +var _anim: Node = null +var _spin_deg := 0.0 + + +func _ready() -> void: + _spin_deg = float(OS.get_environment("AGP_PREVIEW_SPIN")) + + var env := WorldEnvironment.new() + var e := Environment.new() + e.background_mode = Environment.BG_COLOR + e.background_color = Color(0.16, 0.18, 0.22) + e.ambient_light_source = Environment.AMBIENT_SOURCE_COLOR + e.ambient_light_color = Color(0.6, 0.64, 0.7) + e.ambient_light_energy = 0.6 + env.environment = e + add_child(env) + + var key := DirectionalLight3D.new() + key.rotation = Vector3(-0.9, -0.6, 0.0) + key.light_energy = 1.1 + add_child(key) + + var ground := MeshInstance3D.new() + var pm := PlaneMesh.new() + pm.size = Vector2(6, 6) + ground.mesh = pm + var gmat := StandardMaterial3D.new() + gmat.albedo_color = Color(0.3, 0.32, 0.36) + ground.material_override = gmat + add_child(ground) + + var cam := Camera3D.new() + cam.position = Vector3(2.1, 1.15, 2.6) + cam.look_at_from_position(cam.position, Vector3(0.0, 1.0, 0.0), Vector3.UP) + cam.current = true + add_child(cam) + + var avatars := get_node_or_null("/root/AvatarSystem") + if avatars != null and avatars.has_method("build_current_avatar"): + _avatar = avatars.build_current_avatar() + if _avatar != null: + add_child(_avatar) + var skel := _avatar.find_child("Skeleton3D", true, false) + if skel is Skeleton3D and OS.get_environment("AGP_PREVIEW_WALK") == "1": + _anim = LimbAnimator.new() + add_child(_anim) + _anim.call("setup", skel) + _anim.set("speed", 4.2) + else: + push_warning("avatar_preview: AvatarSystem.build_current_avatar() returned null") + + +func _process(delta: float) -> void: + if _avatar != null and _spin_deg != 0.0: + _avatar.rotate_y(deg_to_rad(_spin_deg) * delta) diff --git a/scripts/tools/gen_placeholder_avatars.gd b/scripts/tools/gen_placeholder_avatars.gd index 7089e2d..cf02fe6 100644 --- a/scripts/tools/gen_placeholder_avatars.gd +++ b/scripts/tools/gen_placeholder_avatars.gd @@ -19,29 +19,29 @@ const OUT_DIR := "res://assets/avatars/" # name -> [parent_name, global_rest_position] (A-pose, arms down, Y-up, metres) const BONES := { "Root": ["", Vector3(0.00, 0.00, 0.00)], - "Hips": ["Root", Vector3(0.00, 0.80, 0.00)], + "Hips": ["Root", Vector3(0.00, 0.76, 0.00)], "Spine": ["Hips", Vector3(0.00, 1.00, 0.00)], - "Chest": ["Spine", Vector3(0.00, 1.22, 0.00)], - "Neck": ["Chest", Vector3(0.00, 1.36, 0.00)], - "Head": ["Neck", Vector3(0.00, 1.44, 0.00)], - "HeadTop": ["Head", Vector3(0.00, 1.92, 0.00)], - "Jaw": ["Head", Vector3(0.00, 1.54, 0.14)], - "LeftEye": ["Head", Vector3(0.12, 1.66, 0.28)], - "RightEye": ["Head", Vector3(-0.12, 1.66, 0.28)], - "LeftShoulder": ["Chest", Vector3(0.22, 1.34, 0.00)], - "LeftUpperArm": ["LeftShoulder", Vector3(0.45, 1.30, 0.00)], - "LeftLowerArm": ["LeftUpperArm", Vector3(0.45, 1.02, 0.00)], - "LeftHand": ["LeftLowerArm", Vector3(0.45, 0.74, 0.00)], - "RightShoulder": ["Chest", Vector3(-0.22, 1.34, 0.00)], - "RightUpperArm": ["RightShoulder", Vector3(-0.45, 1.30, 0.00)], - "RightLowerArm": ["RightUpperArm", Vector3(-0.45, 1.02, 0.00)], - "RightHand": ["RightLowerArm", Vector3(-0.45, 0.74, 0.00)], - "LeftUpperLeg": ["Hips", Vector3(0.17, 0.78, 0.00)], - "LeftLowerLeg": ["LeftUpperLeg", Vector3(0.17, 0.40, 0.00)], - "LeftFoot": ["LeftLowerLeg", Vector3(0.17, 0.05, 0.05)], - "RightUpperLeg": ["Hips", Vector3(-0.17, 0.78, 0.00)], - "RightLowerLeg": ["RightUpperLeg", Vector3(-0.17, 0.40, 0.00)], - "RightFoot": ["RightLowerLeg", Vector3(-0.17, 0.05, 0.05)], + "Chest": ["Spine", Vector3(0.00, 1.30, 0.00)], + "Neck": ["Chest", Vector3(0.00, 1.50, 0.00)], + "Head": ["Neck", Vector3(0.00, 1.58, 0.00)], + "HeadTop": ["Head", Vector3(0.00, 2.00, 0.00)], + "Jaw": ["Head", Vector3(0.00, 1.64, 0.16)], + "LeftEye": ["Head", Vector3(0.12, 1.76, 0.27)], + "RightEye": ["Head", Vector3(-0.12, 1.76, 0.27)], + "LeftShoulder": ["Chest", Vector3(0.42, 1.50, 0.00)], + "LeftUpperArm": ["LeftShoulder", Vector3(0.60, 1.44, 0.00)], + "LeftLowerArm": ["LeftUpperArm", Vector3(0.60, 1.14, 0.00)], + "LeftHand": ["LeftLowerArm", Vector3(0.60, 0.82, 0.00)], + "RightShoulder": ["Chest", Vector3(-0.42, 1.50, 0.00)], + "RightUpperArm": ["RightShoulder", Vector3(-0.60, 1.44, 0.00)], + "RightLowerArm": ["RightUpperArm", Vector3(-0.60, 1.14, 0.00)], + "RightHand": ["RightLowerArm", Vector3(-0.60, 0.82, 0.00)], + "LeftUpperLeg": ["Hips", Vector3(0.21, 0.74, 0.00)], + "LeftLowerLeg": ["LeftUpperLeg", Vector3(0.21, 0.38, 0.00)], + "LeftFoot": ["LeftLowerLeg", Vector3(0.21, 0.05, 0.06)], + "RightUpperLeg": ["Hips", Vector3(-0.21, 0.74, 0.00)], + "RightLowerLeg": ["RightUpperLeg", Vector3(-0.21, 0.38, 0.00)], + "RightFoot": ["RightLowerLeg", Vector3(-0.21, 0.05, 0.06)], } # Order matters: parents before children. @@ -64,16 +64,16 @@ const C_FACE := Color(0.05, 0.05, 0.06) # near-black (eyes + smile) # weighted to its bone -- so the rig still drives it, but it reads as a clean # solid block (no banding, no capsule taper). const PARTS := [ - ["Head", Vector3(0.00, 1.62, 0.00), Vector3(0.60, 0.56, 0.56), C_SKIN], - ["Spine", Vector3(0.00, 1.06, 0.00), Vector3(0.74, 0.64, 0.38), C_SHIRT], - ["LeftUpperArm", Vector3(0.45, 1.04, 0.00), Vector3(0.27, 0.66, 0.31), C_SKIN], - ["RightUpperArm", Vector3(-0.45, 1.04, 0.00), Vector3(0.27, 0.66, 0.31), C_SKIN], - ["LeftUpperLeg", Vector3(0.17, 0.39, 0.00), Vector3(0.31, 0.80, 0.33), C_PANTS], - ["RightUpperLeg", Vector3(-0.17, 0.39, 0.00), Vector3(0.31, 0.80, 0.33), C_PANTS], + ["Head", Vector3(0.00, 1.74, 0.00), Vector3(0.58, 0.56, 0.56), C_SKIN], + ["Spine", Vector3(0.00, 1.14, 0.00), Vector3(0.90, 0.80, 0.48), C_SHIRT], + ["LeftUpperArm", Vector3(0.58, 1.14, 0.00), Vector3(0.34, 0.78, 0.40), C_SKIN], + ["RightUpperArm", Vector3(-0.58, 1.14, 0.00), Vector3(0.34, 0.78, 0.40), C_SKIN], + ["LeftUpperLeg", Vector3(0.21, 0.38, 0.00), Vector3(0.40, 0.78, 0.40), C_PANTS], + ["RightUpperLeg", Vector3(-0.21, 0.38, 0.00), Vector3(0.40, 0.78, 0.40), C_PANTS], # painted-on face -- thin plates on the front of the head (+Z), rigged to Head - ["Head", Vector3(0.12, 1.66, 0.285), Vector3(0.07, 0.10, 0.02), C_FACE], - ["Head", Vector3(-0.12, 1.66, 0.285), Vector3(0.07, 0.10, 0.02), C_FACE], - ["Head", Vector3(0.00, 1.52, 0.285), Vector3(0.26, 0.05, 0.02), C_FACE], + ["Head", Vector3(0.13, 1.78, 0.28), Vector3(0.09, 0.12, 0.02), C_FACE], + ["Head", Vector3(-0.13, 1.78, 0.28), Vector3(0.09, 0.12, 0.02), C_FACE], + ["Head", Vector3(0.00, 1.64, 0.28), Vector3(0.32, 0.05, 0.02), C_FACE], ] # Attachments: id -> [Mesh, local_offset_from_mount_bone, colour] @@ -90,8 +90,8 @@ const ATTACHMENTS := { "right_hand_glove_01": ["blob", Vector3(0, 0.0, 0.0), Color(0.15, 0.15, 0.18)], "left_foot_boot_01": ["boot", Vector3(0, -0.02, 0.05), Color(0.12, 0.10, 0.09)], "right_foot_boot_01": ["boot", Vector3(0, -0.02, 0.05), Color(0.12, 0.10, 0.09)], - "accessory_belt_01": ["belt", Vector3(0, 0.80, 0.0), Color(0.20, 0.18, 0.12)], - "accessory_necklace_01": ["necklace", Vector3(0, 1.30, 0.06), Color(0.85, 0.75, 0.20)], + "accessory_belt_01": ["belt", Vector3(0, 0.74, 0.0), Color(0.20, 0.18, 0.12)], + "accessory_necklace_01": ["necklace", Vector3(0, 1.34, 0.10), Color(0.85, 0.75, 0.20)], } var _fail := 0 diff --git a/scripts/tools/screenshot_rig.gd b/scripts/tools/screenshot_rig.gd new file mode 100644 index 0000000..329a43d --- /dev/null +++ b/scripts/tools/screenshot_rig.gd @@ -0,0 +1,72 @@ +extends Node +## Dev-only screenshot harness. Boots a target scene inside the real (GPU) +## renderer, lets it run a few frames, then reads the rendered frame back off +## the viewport and writes it to disk. Nothing in the shipped game touches this. +## +## Driven entirely by environment variables so it needs no CLI parsing: +## AGP_SHOT_SCENE res:// path of the scene to capture (default: game.tscn) +## AGP_SHOT_OUT output PNG path (absolute) (default: /tmp/agp_shot.png) +## AGP_SHOT_FRAMES frames to settle before the first shot (default: 45) +## AGP_SHOT_SEQ "count:gap" -> take shots frames apart, +## suffixing the filename _01.._NN (default: single shot) +## +## Launch (hardware Vulkan on cloudbison's Radeon, via Xvfb): +## Xvfb :99 -screen 0 1280x720x24 & +## sg render -c "sg video -c 'DISPLAY=:99 AGP_SHOT_SCENE=res://scenes/game.tscn \ +## AGP_SHOT_OUT=/abs/shot.png ~/godot/godot --path . \ +## --rendering-driver vulkan --gpu-index 0 --resolution 1280x720 \ +## res://scenes/dev/screenshot_rig.tscn'" + +func _ready() -> void: + _run.call_deferred() + + +func _run() -> void: + var target := OS.get_environment("AGP_SHOT_SCENE") + if target.is_empty(): + target = "res://scenes/game.tscn" + var out := OS.get_environment("AGP_SHOT_OUT") + if out.is_empty(): + out = "/tmp/agp_shot.png" + var settle := int(OS.get_environment("AGP_SHOT_FRAMES")) + if settle <= 0: + settle = 45 + + var seq_count := 1 + var seq_gap := 0 + var seq_env := OS.get_environment("AGP_SHOT_SEQ") + if seq_env.contains(":"): + var parts := seq_env.split(":") + seq_count = maxi(1, int(parts[0])) + seq_gap = maxi(1, int(parts[1])) + + var packed: PackedScene = load(target) + if packed == null: + push_error("screenshot_rig: cannot load %s" % target) + get_tree().quit(1) + return + var inst: Node = packed.instantiate() + get_tree().root.add_child(inst) + get_tree().current_scene = inst + + for i in settle: + await get_tree().process_frame + + for shot in seq_count: + # get_image() on the viewport texture is a GPU->CPU readback of the + # frame that was just rendered -- no window presentation needed, which + # is why this works over Xvfb (no DRI3). + var img: Image = get_viewport().get_texture().get_image() + var path := out + if seq_count > 1: + path = "%s_%02d.png" % [out.get_basename(), shot + 1] + var err := img.save_png(path) + if err != OK: + push_error("screenshot_rig: save_png(%s) failed: %d" % [path, err]) + else: + print("SHOT %s (%dx%d)" % [path, img.get_width(), img.get_height()]) + if shot < seq_count - 1: + for j in seq_gap: + await get_tree().process_frame + + get_tree().quit()