|
|
|
@@ -10,6 +10,7 @@ var weapons: Dictionary = {} # slot_index: Node3D
|
|
|
|
|
var canvas_layer: CanvasLayer
|
|
|
|
|
var sub_viewport: SubViewport
|
|
|
|
|
var vm_camera: Camera3D
|
|
|
|
|
var _viewmodel_skin_override := ""
|
|
|
|
|
|
|
|
|
|
func _ready() -> void:
|
|
|
|
|
set_process_input(true)
|
|
|
|
@@ -21,6 +22,9 @@ func _ready() -> void:
|
|
|
|
|
var nm = get_node_or_null("/root/NetworkManager")
|
|
|
|
|
if nm and nm.has_signal("ladder_promoted"):
|
|
|
|
|
nm.ladder_promoted.connect(_on_ladder_promoted)
|
|
|
|
|
var skin_manager = get_node_or_null("/root/SkinManager")
|
|
|
|
|
if skin_manager and skin_manager.has_signal("skin_changed"):
|
|
|
|
|
skin_manager.skin_changed.connect(_on_skin_changed)
|
|
|
|
|
|
|
|
|
|
# Wait one frame for LoadoutManager to be fully ready if needed
|
|
|
|
|
await get_tree().process_frame
|
|
|
|
@@ -65,22 +69,42 @@ func _setup_viewmodel_viewport() -> void:
|
|
|
|
|
vm_camera.near = 0.01 # viewmodel sits close to the lens; avoid clipping
|
|
|
|
|
sub_viewport.add_child(vm_camera)
|
|
|
|
|
|
|
|
|
|
# Add a light specifically for the viewmodel in case main lights don't reach
|
|
|
|
|
var dir_light = DirectionalLight3D.new()
|
|
|
|
|
dir_light.layers = 1 << 19
|
|
|
|
|
dir_light.rotation_degrees = Vector3(-60, 45, 0)
|
|
|
|
|
dir_light.light_energy = 0.5
|
|
|
|
|
add_child(dir_light)
|
|
|
|
|
|
|
|
|
|
# Camera-relative two-light rig. The previous light lived in world space, so
|
|
|
|
|
# merely turning the player could put the receiver on its dark side and
|
|
|
|
|
# collapse the whole weapon into a black silhouette. Parenting these to the
|
|
|
|
|
# viewmodel camera makes their graphic key/fill relationship stable in every
|
|
|
|
|
# direction, like lighting an animation cel rather than a world prop.
|
|
|
|
|
var key_light := DirectionalLight3D.new()
|
|
|
|
|
key_light.name = "ViewmodelKey"
|
|
|
|
|
key_light.light_cull_mask = 1 << 19
|
|
|
|
|
key_light.rotation_degrees = Vector3(-34, -42, 0)
|
|
|
|
|
key_light.light_color = Color(1.0, 0.93, 0.84)
|
|
|
|
|
# The shader carries the graphic four-tone key. This real light only gives
|
|
|
|
|
# the narrow custom specular something to react to; the previous energy of
|
|
|
|
|
# 3.0 flooded every material into the same plastic middle grey.
|
|
|
|
|
key_light.light_energy = 0.80
|
|
|
|
|
key_light.shadow_enabled = false
|
|
|
|
|
vm_camera.add_child(key_light)
|
|
|
|
|
|
|
|
|
|
var fill_light := DirectionalLight3D.new()
|
|
|
|
|
fill_light.name = "ViewmodelFill"
|
|
|
|
|
fill_light.light_cull_mask = 1 << 19
|
|
|
|
|
fill_light.rotation_degrees = Vector3(18, 148, 0)
|
|
|
|
|
fill_light.light_color = Color(0.46, 0.62, 1.0)
|
|
|
|
|
fill_light.light_energy = 0.045
|
|
|
|
|
fill_light.shadow_enabled = false
|
|
|
|
|
vm_camera.add_child(fill_light)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
var _bob_timer: float = 0.0
|
|
|
|
|
var _current_slide_tilt_z: float = 0.0
|
|
|
|
|
var _current_slide_tilt_x: float = 0.0
|
|
|
|
|
var _current_slide_offset_x: float = 0.0
|
|
|
|
|
var _current_slide_offset_y: float = 0.0
|
|
|
|
|
var _recoil_pitch: float = 0.0
|
|
|
|
|
var _recoil_yaw: float = 0.0
|
|
|
|
|
var _target_drift_offset: Vector3 = Vector3.ZERO
|
|
|
|
|
var _current_drift_offset: Vector3 = Vector3.ZERO
|
|
|
|
|
var _movement_pose_position := Vector3.ZERO
|
|
|
|
|
var _movement_pose_rotation := Vector3.ZERO
|
|
|
|
|
var _movement_pose_state := "idle"
|
|
|
|
|
var _movement_pose_time := 0.0
|
|
|
|
|
var _swap_pitch: float = 0.0
|
|
|
|
|
var _reload_dip: float = 0.0
|
|
|
|
|
|
|
|
|
@@ -102,25 +126,38 @@ func _process(_delta: float) -> void:
|
|
|
|
|
var over_speed_factor = clampf((hspeed - 11.0) / 19.0, 0.0, 1.0) # maxes out at 30 m/s
|
|
|
|
|
target_fov = lerpf(72.0, 80.0, over_speed_factor)
|
|
|
|
|
|
|
|
|
|
# Weapon Viewmodel Bobbing, Drift, and Slide Tilt
|
|
|
|
|
var is_sliding = player.has_method("get_node") and player.get_node_or_null("MovementStateMachine") and player.get_node("MovementStateMachine").current_state == "slide"
|
|
|
|
|
|
|
|
|
|
if is_sliding:
|
|
|
|
|
_bob_timer = 0.0
|
|
|
|
|
# Target tilt when sliding (inwards towards center, pitched slightly down)
|
|
|
|
|
# Assuming weapon is on right side (standard FPS), tilting left (positive Z rotation)
|
|
|
|
|
_current_slide_tilt_z = lerpf(_current_slide_tilt_z, deg_to_rad(15.0), 12.0 * _delta)
|
|
|
|
|
_current_slide_tilt_x = lerpf(_current_slide_tilt_x, 0.0, 12.0 * _delta)
|
|
|
|
|
# Move closer to center (negative X) and slightly down (negative Y)
|
|
|
|
|
_current_slide_offset_x = lerpf(_current_slide_offset_x, -0.15, 12.0 * _delta)
|
|
|
|
|
_current_slide_offset_y = lerpf(_current_slide_offset_y, -0.1, 12.0 * _delta)
|
|
|
|
|
# Authored first-person handling. The movement state changes HOW the
|
|
|
|
|
# character carries the gun; recoil/reload/ADS still layer afterward.
|
|
|
|
|
var machine = player.get_node_or_null("MovementStateMachine")
|
|
|
|
|
var state: String = machine.current_state if machine else "idle"
|
|
|
|
|
if state != _movement_pose_state:
|
|
|
|
|
_movement_pose_state = state
|
|
|
|
|
_movement_pose_time = 0.0
|
|
|
|
|
else:
|
|
|
|
|
_current_slide_tilt_z = lerpf(_current_slide_tilt_z, 0.0, 10.0 * _delta)
|
|
|
|
|
_current_slide_tilt_x = lerpf(_current_slide_tilt_x, 0.0, 10.0 * _delta)
|
|
|
|
|
_current_slide_offset_x = lerpf(_current_slide_offset_x, 0.0, 10.0 * _delta)
|
|
|
|
|
_current_slide_offset_y = lerpf(_current_slide_offset_y, 0.0, 10.0 * _delta)
|
|
|
|
|
|
|
|
|
|
if player.is_on_floor() and hspeed > 1.0:
|
|
|
|
|
_movement_pose_time += _delta
|
|
|
|
|
var local_vel := camera.global_transform.basis.inverse() * player.velocity
|
|
|
|
|
var state_pose := _movement_pose_target(
|
|
|
|
|
state, machine, local_vel, hspeed)
|
|
|
|
|
var target_position: Vector3 = state_pose[0]
|
|
|
|
|
var target_rotation: Vector3 = state_pose[1]
|
|
|
|
|
var active_weapon = weapons.get(active_slot)
|
|
|
|
|
var ads_weight := 1.0 if active_weapon \
|
|
|
|
|
and "is_ads" in active_weapon and active_weapon.is_ads else 0.0
|
|
|
|
|
# Keep sights readable without erasing all physical response.
|
|
|
|
|
target_position *= lerpf(1.0, 0.28, ads_weight)
|
|
|
|
|
target_rotation *= lerpf(1.0, 0.24, ads_weight)
|
|
|
|
|
var pose_rate := _movement_pose_rate(state)
|
|
|
|
|
var pose_blend := 1.0 - exp(-pose_rate * _delta)
|
|
|
|
|
_movement_pose_position = _movement_pose_position.lerp(
|
|
|
|
|
target_position, pose_blend)
|
|
|
|
|
_movement_pose_rotation = _lerp_euler(
|
|
|
|
|
_movement_pose_rotation, target_rotation, pose_blend)
|
|
|
|
|
|
|
|
|
|
if state == "slide":
|
|
|
|
|
_bob_timer = 0.0
|
|
|
|
|
else:
|
|
|
|
|
if state in ["ground", "idle"] and player.is_on_floor() \
|
|
|
|
|
and hspeed > 1.0:
|
|
|
|
|
# Scale bobbing frequency by walk speed
|
|
|
|
|
_bob_timer += _delta * 12.0 * (hspeed / 11.0)
|
|
|
|
|
var bob_y = sin(_bob_timer) * 0.015
|
|
|
|
@@ -132,8 +169,8 @@ func _process(_delta: float) -> void:
|
|
|
|
|
if not player.is_on_floor():
|
|
|
|
|
# Airborne Drift
|
|
|
|
|
# Convert player velocity into camera's local space
|
|
|
|
|
var local_vel = camera.global_transform.basis.inverse() * player.velocity
|
|
|
|
|
_target_drift_offset = Vector3(local_vel.x * 0.004, local_vel.y * 0.004, 0)
|
|
|
|
|
_target_drift_offset = Vector3(
|
|
|
|
|
local_vel.x * 0.0025, local_vel.y * 0.0025, 0)
|
|
|
|
|
# Clamp the drift
|
|
|
|
|
_target_drift_offset.x = clampf(_target_drift_offset.x, -0.05, 0.05)
|
|
|
|
|
_target_drift_offset.y = clampf(_target_drift_offset.y, -0.05, 0.05)
|
|
|
|
@@ -141,17 +178,23 @@ func _process(_delta: float) -> void:
|
|
|
|
|
_target_drift_offset = Vector3.ZERO
|
|
|
|
|
|
|
|
|
|
# Smoothly apply drift so jumping/landing doesn't snap
|
|
|
|
|
_current_drift_offset = _current_drift_offset.lerp(_target_drift_offset, 15.0 * _delta)
|
|
|
|
|
_current_drift_offset = _current_drift_offset.lerp(
|
|
|
|
|
_target_drift_offset, 1.0 - exp(-15.0 * _delta))
|
|
|
|
|
|
|
|
|
|
vm_camera.translate_object_local(_current_drift_offset)
|
|
|
|
|
vm_camera.rotate_object_local(Vector3.UP, deg_to_rad(-_current_drift_offset.x * 30.0))
|
|
|
|
|
vm_camera.rotate_object_local(Vector3.RIGHT, deg_to_rad(_current_drift_offset.y * 30.0))
|
|
|
|
|
|
|
|
|
|
# Apply slide tilt and offset
|
|
|
|
|
if absf(_current_slide_tilt_z) > 0.001 or absf(_current_slide_offset_x) > 0.001:
|
|
|
|
|
vm_camera.translate_object_local(Vector3(_current_slide_offset_x, _current_slide_offset_y, 0))
|
|
|
|
|
vm_camera.rotate_object_local(Vector3.FORWARD, _current_slide_tilt_z)
|
|
|
|
|
vm_camera.rotate_object_local(Vector3.RIGHT, _current_slide_tilt_x)
|
|
|
|
|
|
|
|
|
|
# The targets above describe the WEAPON'S desired movement. Moving
|
|
|
|
|
# the isolated viewmodel camera by the inverse composes that pose
|
|
|
|
|
# with every weapon without overwriting weapon-local reload tweens.
|
|
|
|
|
vm_camera.translate_object_local(-_movement_pose_position)
|
|
|
|
|
vm_camera.rotate_object_local(
|
|
|
|
|
Vector3.RIGHT, -_movement_pose_rotation.x)
|
|
|
|
|
vm_camera.rotate_object_local(
|
|
|
|
|
Vector3.UP, -_movement_pose_rotation.y)
|
|
|
|
|
vm_camera.rotate_object_local(
|
|
|
|
|
Vector3.FORWARD, -_movement_pose_rotation.z)
|
|
|
|
|
|
|
|
|
|
# Visual Recoil
|
|
|
|
|
_recoil_pitch = lerpf(_recoil_pitch, 0.0, 15.0 * _delta)
|
|
|
|
@@ -184,6 +227,90 @@ func _process(_delta: float) -> void:
|
|
|
|
|
|
|
|
|
|
vm_camera.fov = lerpf(vm_camera.fov, target_fov, 10.0 * _delta)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
func _movement_pose_target(state: String, machine: Node,
|
|
|
|
|
local_velocity: Vector3, speed: float) -> Array[Vector3]:
|
|
|
|
|
var position := Vector3.ZERO
|
|
|
|
|
var rotation := Vector3.ZERO
|
|
|
|
|
var phase := _movement_pose_time
|
|
|
|
|
match state:
|
|
|
|
|
"ground", "idle":
|
|
|
|
|
if machine and machine.input_crouch:
|
|
|
|
|
position = Vector3(-0.035, -0.035, 0.018)
|
|
|
|
|
rotation = Vector3(3.0, -2.0, 3.0)
|
|
|
|
|
"air":
|
|
|
|
|
if local_velocity.y > 0.2:
|
|
|
|
|
var rise := clampf(local_velocity.y / 10.0, 0.0, 1.0)
|
|
|
|
|
position = Vector3(0.035, -0.045, 0.035 + 0.025 * rise)
|
|
|
|
|
rotation = Vector3(6.0 + 3.0 * rise, -4.0, 3.0)
|
|
|
|
|
else:
|
|
|
|
|
var fall := clampf(-local_velocity.y / 15.0, 0.0, 1.0)
|
|
|
|
|
position = Vector3(-0.018, 0.008 + 0.018 * fall, 0.025)
|
|
|
|
|
rotation = Vector3(-2.0 - 3.0 * fall, 3.0, -2.0)
|
|
|
|
|
rotation.z += sin(phase * 4.0) * 0.8
|
|
|
|
|
"dash":
|
|
|
|
|
# Drop and tuck right so the burst clears the crosshair. A negative
|
|
|
|
|
# roll preserves the weapon's low diagonal instead of standing a
|
|
|
|
|
# long gun vertically through the center of the screen.
|
|
|
|
|
position = Vector3(0.12, -0.10, 0.080)
|
|
|
|
|
rotation = Vector3(8.0, 4.0, -10.0)
|
|
|
|
|
"slide":
|
|
|
|
|
position = Vector3(0.14, -0.12, 0.055)
|
|
|
|
|
position.y += sin(phase * 15.0) * 0.003
|
|
|
|
|
rotation = Vector3(8.0, 3.0, -12.0)
|
|
|
|
|
"wall_run":
|
|
|
|
|
var side: float = machine.wall_side if machine else 0.0
|
|
|
|
|
if is_zero_approx(side):
|
|
|
|
|
side = 1.0
|
|
|
|
|
position = Vector3(-side * 0.085, -0.035, 0.060)
|
|
|
|
|
position.y += sin(phase * 10.0) * 0.006
|
|
|
|
|
rotation = Vector3(
|
|
|
|
|
7.0, -side * 6.0, -side * 15.0
|
|
|
|
|
+ sin(phase * 7.0) * 1.2)
|
|
|
|
|
"wall_cling", "wall_climb":
|
|
|
|
|
var side: float = machine.wall_side if machine else 1.0
|
|
|
|
|
position = Vector3(-side * 0.11, -0.095, 0.10)
|
|
|
|
|
rotation = Vector3(15.0, -side * 9.0, -side * 18.0)
|
|
|
|
|
"grapple":
|
|
|
|
|
var pull := clampf(speed / 20.0, 0.0, 1.0)
|
|
|
|
|
position = Vector3(0.11, -0.060, 0.065 + 0.025 * pull)
|
|
|
|
|
position.y += sin(phase * 5.0) * 0.006
|
|
|
|
|
rotation = Vector3(7.0, 11.0, -15.0)
|
|
|
|
|
_:
|
|
|
|
|
pass
|
|
|
|
|
return [position, rotation * (PI / 180.0)]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
func _movement_pose_rate(state: String) -> float:
|
|
|
|
|
match state:
|
|
|
|
|
"dash":
|
|
|
|
|
return 22.0
|
|
|
|
|
"slide":
|
|
|
|
|
return 16.0
|
|
|
|
|
"wall_run", "wall_cling", "wall_climb":
|
|
|
|
|
return 11.0
|
|
|
|
|
"grapple":
|
|
|
|
|
return 10.0
|
|
|
|
|
"air":
|
|
|
|
|
return 8.0
|
|
|
|
|
_:
|
|
|
|
|
return 9.0
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
func _lerp_euler(from: Vector3, to: Vector3, weight: float) -> Vector3:
|
|
|
|
|
return Vector3(
|
|
|
|
|
lerp_angle(from.x, to.x, weight),
|
|
|
|
|
lerp_angle(from.y, to.y, weight),
|
|
|
|
|
lerp_angle(from.z, to.z, weight))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
func movement_pose_debug() -> Dictionary:
|
|
|
|
|
return {
|
|
|
|
|
"state": _movement_pose_state,
|
|
|
|
|
"position": _movement_pose_position,
|
|
|
|
|
"rotation": _movement_pose_rotation,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func _build_loadout() -> void:
|
|
|
|
|
var is_auth = false
|
|
|
|
|
if player:
|
|
|
|
@@ -305,6 +432,14 @@ func _spawn_weapon(slot: int, weapon_id: String) -> void:
|
|
|
|
|
# the world's look. No inverted-hull outline: FBX weapon models have
|
|
|
|
|
# hard-edged normals that make the hull tear into scratchy artifacts.
|
|
|
|
|
LevelMaterials.apply_toon_recursive(w, 0.0)
|
|
|
|
|
# Toon conversion gives every semantic hand piece its own material.
|
|
|
|
|
# Apply the selected glove/bare/android style after that split so a
|
|
|
|
|
# shared construction material cannot make the last fingertip color
|
|
|
|
|
# overwrite the palm.
|
|
|
|
|
for child in w.get_children():
|
|
|
|
|
if child.has_meta("viewmodel_arm"):
|
|
|
|
|
_restyle_arm(child)
|
|
|
|
|
LevelMaterials.apply_viewmodel_look(w)
|
|
|
|
|
|
|
|
|
|
# Set weapons to viewmodel layer
|
|
|
|
|
_set_layer_recursive(w, 1 << 19)
|
|
|
|
@@ -328,54 +463,335 @@ func _add_procedural_arms(weapon: Node3D) -> void:
|
|
|
|
|
# the gun (model_root) and each hand (named pivots) independently.
|
|
|
|
|
# Hand points come from WeaponGrips so the third-person model reaches for
|
|
|
|
|
# exactly the same spots this viewmodel uses.
|
|
|
|
|
_build_arm(weapon, Vector3(0.25, -0.3, 0.5), WeaponGrips.GRIP, "ArmR")
|
|
|
|
|
var grip := WeaponGrips.GRIP
|
|
|
|
|
if "weapon_name" in weapon and weapon.weapon_name == "Knife":
|
|
|
|
|
# The knife FBX's knuckle-guard grip is offset from the gun-set's
|
|
|
|
|
# pistol-grip origin. Put the articulated fingers around the orange
|
|
|
|
|
# handle instead of leaving the hand floating behind it.
|
|
|
|
|
grip = Vector3(0.17, 0.02, 0.05)
|
|
|
|
|
_build_arm(weapon, Vector3(0.25, -0.3, 0.5), grip, "ArmR")
|
|
|
|
|
if "weapon_name" in weapon and weapon.weapon_name != "Knife":
|
|
|
|
|
_build_arm(weapon, Vector3(-0.25, -0.3, 0.4), WeaponGrips.SUPPORT, "ArmL")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
## A first-person arm styled after the character skin: dark detached sleeve,
|
|
|
|
|
## glowing cuff, bare hand — instead of the old featureless blue slab.
|
|
|
|
|
func _build_arm(weapon: Node3D, shoulder: Vector3, hand: Vector3, arm_name: String = "") -> void:
|
|
|
|
|
var sleeve_mat = StandardMaterial3D.new()
|
|
|
|
|
sleeve_mat.albedo_color = Color(0.10, 0.11, 0.14) # near-black sleeve
|
|
|
|
|
sleeve_mat.roughness = 0.8
|
|
|
|
|
var cuff_mat = StandardMaterial3D.new()
|
|
|
|
|
cuff_mat.albedo_color = Color(0.25, 0.95, 0.90) # signature teal cuff
|
|
|
|
|
cuff_mat.emission_enabled = true
|
|
|
|
|
cuff_mat.emission = Color(0.10, 0.55, 0.52)
|
|
|
|
|
var skin_mat = StandardMaterial3D.new()
|
|
|
|
|
skin_mat.albedo_color = Color(0.98, 0.88, 0.82) # skin
|
|
|
|
|
skin_mat.roughness = 0.9
|
|
|
|
|
## Rebuild the lightweight first-person arms as soon as the local selection
|
|
|
|
|
## changes; each weapon owns its own animated ArmR/ArmL pivots.
|
|
|
|
|
func _on_skin_changed(skin_id: String) -> void:
|
|
|
|
|
# The signal fires immediately before the player controller mirrors the
|
|
|
|
|
# selection into synced_skin_id, so use its payload for this rebuild.
|
|
|
|
|
_viewmodel_skin_override = skin_id
|
|
|
|
|
for weapon in weapons.values():
|
|
|
|
|
if not is_instance_valid(weapon):
|
|
|
|
|
continue
|
|
|
|
|
var found_arm := false
|
|
|
|
|
for child in weapon.get_children():
|
|
|
|
|
if child.has_meta("viewmodel_arm"):
|
|
|
|
|
found_arm = true
|
|
|
|
|
_restyle_arm(child)
|
|
|
|
|
if not found_arm:
|
|
|
|
|
_add_procedural_arms(weapon)
|
|
|
|
|
LevelMaterials.apply_toon_recursive(weapon, 0.0)
|
|
|
|
|
LevelMaterials.apply_viewmodel_look(weapon)
|
|
|
|
|
_set_layer_recursive(weapon, 1 << 19)
|
|
|
|
|
_viewmodel_skin_override = ""
|
|
|
|
|
|
|
|
|
|
var pivot = Node3D.new()
|
|
|
|
|
|
|
|
|
|
func _viewmodel_skin() -> PlayerSkin:
|
|
|
|
|
var skin_manager = get_node_or_null("/root/SkinManager")
|
|
|
|
|
if skin_manager == null:
|
|
|
|
|
return null
|
|
|
|
|
var skin_id: String = _viewmodel_skin_override
|
|
|
|
|
if skin_id.is_empty() and is_instance_valid(player) and "synced_skin_id" in player:
|
|
|
|
|
skin_id = player.synced_skin_id
|
|
|
|
|
if skin_id.is_empty():
|
|
|
|
|
skin_id = skin_manager.active_skin_id
|
|
|
|
|
return skin_manager.get_skin(skin_id)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
func _set_arm_piece_color(piece: MeshInstance3D, color: Color) -> void:
|
|
|
|
|
if piece == null or piece.mesh == null:
|
|
|
|
|
return
|
|
|
|
|
for surface in piece.mesh.get_surface_count():
|
|
|
|
|
var material := piece.get_active_material(surface)
|
|
|
|
|
if material is ShaderMaterial:
|
|
|
|
|
material.set_shader_parameter("albedo_color", color)
|
|
|
|
|
elif material is BaseMaterial3D:
|
|
|
|
|
material.albedo_color = color
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
func _arm_surface_color(surface: String, skin: PlayerSkin) -> Color:
|
|
|
|
|
match surface:
|
|
|
|
|
"sleeve":
|
|
|
|
|
return skin.viewmodel_sleeve_color
|
|
|
|
|
"cuff", "hand_accent":
|
|
|
|
|
return skin.viewmodel_accent_color
|
|
|
|
|
"nail":
|
|
|
|
|
return skin.viewmodel_nail_color
|
|
|
|
|
"forearm":
|
|
|
|
|
return skin.viewmodel_skin_color
|
|
|
|
|
"hand_palm", "finger_prox":
|
|
|
|
|
if skin.viewmodel_hand_style in ["fingerless", "glove"]:
|
|
|
|
|
return skin.viewmodel_glove_color
|
|
|
|
|
if skin.viewmodel_hand_style == "android":
|
|
|
|
|
return skin.viewmodel_accent_color
|
|
|
|
|
return skin.viewmodel_skin_color
|
|
|
|
|
"fingertip":
|
|
|
|
|
if skin.viewmodel_hand_style == "glove":
|
|
|
|
|
return skin.viewmodel_glove_color
|
|
|
|
|
return skin.viewmodel_skin_color
|
|
|
|
|
return skin.viewmodel_skin_color
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
func _arm_surface_visible(surface: String, skin: PlayerSkin) -> bool:
|
|
|
|
|
if surface == "nail":
|
|
|
|
|
return skin.viewmodel_hand_style in ["bare", "fingerless"]
|
|
|
|
|
if surface == "hand_accent":
|
|
|
|
|
return skin.viewmodel_hand_style in ["fingerless", "glove", "android"]
|
|
|
|
|
return true
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
func _restyle_arm(pivot: Node3D) -> void:
|
|
|
|
|
var selected_skin := _viewmodel_skin()
|
|
|
|
|
if selected_skin == null:
|
|
|
|
|
return
|
|
|
|
|
var sleeve := pivot.get_node_or_null("Sleeve") as MeshInstance3D
|
|
|
|
|
var cuff := pivot.get_node_or_null("Cuff") as MeshInstance3D
|
|
|
|
|
var forearm := pivot.get_node_or_null("Forearm") as MeshInstance3D
|
|
|
|
|
for piece in pivot.find_children("*", "MeshInstance3D", true, false):
|
|
|
|
|
var surface: String = str(piece.get_meta("viewmodel_surface", ""))
|
|
|
|
|
if surface.is_empty():
|
|
|
|
|
continue
|
|
|
|
|
piece.visible = _arm_surface_visible(surface, selected_skin)
|
|
|
|
|
_set_arm_piece_color(piece, _arm_surface_color(surface, selected_skin))
|
|
|
|
|
|
|
|
|
|
var arm_length: float = float(pivot.get_meta("arm_length", 0.58))
|
|
|
|
|
var hand_length := minf(0.15, arm_length * 0.25)
|
|
|
|
|
var cuff_length := 0.045
|
|
|
|
|
var clothed_length := clampf(
|
|
|
|
|
(arm_length - hand_length) * selected_skin.viewmodel_sleeve_ratio,
|
|
|
|
|
0.10, arm_length - hand_length - cuff_length)
|
|
|
|
|
var bare_length := maxf(0.0,
|
|
|
|
|
arm_length - clothed_length - cuff_length - hand_length)
|
|
|
|
|
if sleeve:
|
|
|
|
|
sleeve.mesh = _tapered_limb_mesh(
|
|
|
|
|
clothed_length, Vector2(0.068, 0.052), Vector2(0.055, 0.043),
|
|
|
|
|
sleeve.get_active_material(0))
|
|
|
|
|
sleeve.position = Vector3.ZERO
|
|
|
|
|
if cuff:
|
|
|
|
|
cuff.mesh = _tapered_limb_mesh(
|
|
|
|
|
cuff_length, Vector2(0.058, 0.045), Vector2(0.052, 0.040),
|
|
|
|
|
cuff.get_active_material(0))
|
|
|
|
|
cuff.position = Vector3(0, 0, -clothed_length)
|
|
|
|
|
if forearm:
|
|
|
|
|
forearm.visible = bare_length > 0.005
|
|
|
|
|
forearm.mesh = _tapered_limb_mesh(
|
|
|
|
|
maxf(bare_length, 0.01), Vector2(0.050, 0.039),
|
|
|
|
|
Vector2(0.044, 0.034), forearm.get_active_material(0))
|
|
|
|
|
forearm.position = Vector3(0, 0, -(clothed_length + cuff_length))
|
|
|
|
|
var hand_root := pivot.get_node_or_null("Hand") as Node3D
|
|
|
|
|
if hand_root:
|
|
|
|
|
hand_root.position = Vector3(0, 0, -(arm_length - hand_length))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
func _tapered_limb_mesh(length: float, near_radius: Vector2,
|
|
|
|
|
far_radius: Vector2, material: Material) -> ArrayMesh:
|
|
|
|
|
var tool := SurfaceTool.new()
|
|
|
|
|
tool.begin(Mesh.PRIMITIVE_TRIANGLES)
|
|
|
|
|
var sides := 8
|
|
|
|
|
var near_points: Array[Vector3] = []
|
|
|
|
|
var far_points: Array[Vector3] = []
|
|
|
|
|
for i in sides:
|
|
|
|
|
var angle := TAU * float(i) / float(sides) + PI * 0.125
|
|
|
|
|
near_points.append(Vector3(
|
|
|
|
|
cos(angle) * near_radius.x, sin(angle) * near_radius.y, 0))
|
|
|
|
|
far_points.append(Vector3(
|
|
|
|
|
cos(angle) * far_radius.x, sin(angle) * far_radius.y, -length))
|
|
|
|
|
for i in sides:
|
|
|
|
|
var next := (i + 1) % sides
|
|
|
|
|
tool.add_vertex(near_points[i])
|
|
|
|
|
tool.add_vertex(far_points[i])
|
|
|
|
|
tool.add_vertex(far_points[next])
|
|
|
|
|
tool.add_vertex(near_points[i])
|
|
|
|
|
tool.add_vertex(far_points[next])
|
|
|
|
|
tool.add_vertex(near_points[next])
|
|
|
|
|
tool.add_vertex(Vector3.ZERO)
|
|
|
|
|
tool.add_vertex(near_points[next])
|
|
|
|
|
tool.add_vertex(near_points[i])
|
|
|
|
|
tool.add_vertex(Vector3(0, 0, -length))
|
|
|
|
|
tool.add_vertex(far_points[i])
|
|
|
|
|
tool.add_vertex(far_points[next])
|
|
|
|
|
tool.generate_normals()
|
|
|
|
|
var mesh := tool.commit()
|
|
|
|
|
if material:
|
|
|
|
|
mesh.surface_set_material(0, material)
|
|
|
|
|
return mesh
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
func _new_arm_material(color: Color, roughness: float = 0.9) -> StandardMaterial3D:
|
|
|
|
|
var material := StandardMaterial3D.new()
|
|
|
|
|
material.albedo_color = color
|
|
|
|
|
material.roughness = roughness
|
|
|
|
|
return material
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
func _tag_arm_piece(piece: MeshInstance3D, surface: String) -> void:
|
|
|
|
|
piece.set_meta("viewmodel_surface", surface)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
func _add_hand_piece(parent: Node3D, piece_name: String, mesh: Mesh,
|
|
|
|
|
surface: String, position: Vector3 = Vector3.ZERO) -> MeshInstance3D:
|
|
|
|
|
var piece := MeshInstance3D.new()
|
|
|
|
|
piece.name = piece_name
|
|
|
|
|
piece.mesh = mesh
|
|
|
|
|
piece.position = position
|
|
|
|
|
_tag_arm_piece(piece, surface)
|
|
|
|
|
parent.add_child(piece)
|
|
|
|
|
return piece
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
func _capsule_between(parent: Node3D, piece_name: String, from: Vector3,
|
|
|
|
|
to: Vector3, radius: float, material: Material,
|
|
|
|
|
surface: String) -> MeshInstance3D:
|
|
|
|
|
var direction := to - from
|
|
|
|
|
var capsule := CapsuleMesh.new()
|
|
|
|
|
capsule.radius = radius
|
|
|
|
|
capsule.height = maxf(direction.length() + radius * 1.35, radius * 2.05)
|
|
|
|
|
capsule.radial_segments = 8
|
|
|
|
|
capsule.rings = 2
|
|
|
|
|
capsule.material = material
|
|
|
|
|
var piece := _add_hand_piece(
|
|
|
|
|
parent, piece_name, capsule, surface, (from + to) * 0.5)
|
|
|
|
|
if direction.length_squared() > 0.000001:
|
|
|
|
|
piece.basis = Basis(Quaternion(Vector3.UP, direction.normalized()))
|
|
|
|
|
return piece
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
func _build_anime_hand(parent: Node3D, hand_length: float, is_left: bool,
|
|
|
|
|
skin_mat: Material, glove_mat: Material, accent_mat: Material,
|
|
|
|
|
nail_mat: Material) -> void:
|
|
|
|
|
parent.name = "Hand"
|
|
|
|
|
# The palm is a flattened, tapered eight-plane volume. A discrete palm plus
|
|
|
|
|
# articulated digits gives the hand a readable anime silhouette even when
|
|
|
|
|
# the weapon covers half of it.
|
|
|
|
|
var palm := _add_hand_piece(
|
|
|
|
|
parent, "Palm",
|
|
|
|
|
_tapered_limb_mesh(hand_length * 0.62,
|
|
|
|
|
Vector2(0.053, 0.028), Vector2(0.046, 0.023), skin_mat),
|
|
|
|
|
"hand_palm")
|
|
|
|
|
palm.position = Vector3.ZERO
|
|
|
|
|
|
|
|
|
|
var pad_mesh := BoxMesh.new()
|
|
|
|
|
pad_mesh.size = Vector3(0.068, 0.007, hand_length * 0.30)
|
|
|
|
|
pad_mesh.material = accent_mat
|
|
|
|
|
_add_hand_piece(parent, "BackhandPanel", pad_mesh, "hand_accent",
|
|
|
|
|
Vector3(0, 0.028, -hand_length * 0.34))
|
|
|
|
|
|
|
|
|
|
var finger_x := [-0.034, -0.012, 0.012, 0.034]
|
|
|
|
|
var finger_scale := [0.82, 1.0, 0.96, 0.76]
|
|
|
|
|
for i in 4:
|
|
|
|
|
var x: float = finger_x[i]
|
|
|
|
|
var scale_f: float = finger_scale[i]
|
|
|
|
|
var base := Vector3(x, -0.002, -hand_length * 0.52)
|
|
|
|
|
var joint := Vector3(
|
|
|
|
|
x, -0.012, -hand_length * (0.70 + 0.08 * scale_f))
|
|
|
|
|
var tip := Vector3(
|
|
|
|
|
x, -0.047, -hand_length * (0.79 + 0.08 * scale_f))
|
|
|
|
|
var radius := 0.0105 - float(abs(i - 1)) * 0.0008
|
|
|
|
|
_capsule_between(parent, "Finger%dProx" % i, base, joint,
|
|
|
|
|
radius, glove_mat, "finger_prox")
|
|
|
|
|
_capsule_between(parent, "Finger%dTip" % i, joint, tip,
|
|
|
|
|
radius * 0.92, skin_mat, "fingertip")
|
|
|
|
|
var nail_mesh := BoxMesh.new()
|
|
|
|
|
nail_mesh.size = Vector3(radius * 1.15, 0.0035, radius * 1.7)
|
|
|
|
|
nail_mesh.material = nail_mat
|
|
|
|
|
_add_hand_piece(parent, "Nail%d" % i, nail_mesh, "nail",
|
|
|
|
|
tip + Vector3(0, 0.006, 0.002))
|
|
|
|
|
|
|
|
|
|
# Opposed thumb: two separate phalanges angle across the grip instead of
|
|
|
|
|
# reading as a fifth parallel tube.
|
|
|
|
|
var thumb_side := 1.0 if is_left else -1.0
|
|
|
|
|
var thumb_base := Vector3(
|
|
|
|
|
thumb_side * 0.043, -0.002, -hand_length * 0.23)
|
|
|
|
|
var thumb_joint := Vector3(
|
|
|
|
|
thumb_side * 0.069, -0.015, -hand_length * 0.46)
|
|
|
|
|
var thumb_tip := Vector3(
|
|
|
|
|
thumb_side * 0.049, -0.047, -hand_length * 0.62)
|
|
|
|
|
_capsule_between(parent, "ThumbProx", thumb_base, thumb_joint,
|
|
|
|
|
0.0125, glove_mat, "finger_prox")
|
|
|
|
|
_capsule_between(parent, "ThumbTip", thumb_joint, thumb_tip,
|
|
|
|
|
0.0115, skin_mat, "fingertip")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
func _build_arm(weapon: Node3D, shoulder: Vector3, hand: Vector3, arm_name: String = "") -> void:
|
|
|
|
|
var selected_skin := _viewmodel_skin()
|
|
|
|
|
var sleeve_color := selected_skin.viewmodel_sleeve_color \
|
|
|
|
|
if selected_skin else Color(0.16, 0.34, 0.72)
|
|
|
|
|
var accent_color := selected_skin.viewmodel_accent_color \
|
|
|
|
|
if selected_skin else Color(0.28, 0.72, 1.0)
|
|
|
|
|
var hand_color := selected_skin.viewmodel_skin_color \
|
|
|
|
|
if selected_skin else Color(0.96, 0.80, 0.72)
|
|
|
|
|
var glove_color := selected_skin.viewmodel_glove_color \
|
|
|
|
|
if selected_skin else Color(0.10, 0.11, 0.16)
|
|
|
|
|
var nail_color := selected_skin.viewmodel_nail_color \
|
|
|
|
|
if selected_skin else Color(0.92, 0.68, 0.72)
|
|
|
|
|
var sleeve_ratio := selected_skin.viewmodel_sleeve_ratio if selected_skin else 0.68
|
|
|
|
|
|
|
|
|
|
var sleeve_mat := _new_arm_material(sleeve_color, 0.82)
|
|
|
|
|
var cuff_mat := _new_arm_material(accent_color, 0.78)
|
|
|
|
|
var skin_mat := _new_arm_material(hand_color, 0.94)
|
|
|
|
|
var glove_mat := _new_arm_material(glove_color, 0.88)
|
|
|
|
|
var nail_mat := _new_arm_material(nail_color, 0.74)
|
|
|
|
|
|
|
|
|
|
var pivot := Node3D.new()
|
|
|
|
|
if arm_name != "":
|
|
|
|
|
pivot.name = arm_name
|
|
|
|
|
pivot.set_meta("viewmodel_arm", true)
|
|
|
|
|
pivot.set_meta("arm_length", shoulder.distance_to(hand))
|
|
|
|
|
pivot.position = shoulder
|
|
|
|
|
pivot.look_at_from_position(shoulder, hand, Vector3.UP)
|
|
|
|
|
|
|
|
|
|
var sleeve = MeshInstance3D.new()
|
|
|
|
|
var sleeve_mesh = BoxMesh.new()
|
|
|
|
|
sleeve_mesh.size = Vector3(0.075, 0.075, 0.46)
|
|
|
|
|
sleeve_mesh.material = sleeve_mat
|
|
|
|
|
sleeve.mesh = sleeve_mesh
|
|
|
|
|
sleeve.position.z = -0.27
|
|
|
|
|
var arm_length := shoulder.distance_to(hand)
|
|
|
|
|
var hand_length := minf(0.15, arm_length * 0.25)
|
|
|
|
|
var cuff_length := 0.045
|
|
|
|
|
var clothed_length := clampf(
|
|
|
|
|
(arm_length - hand_length) * sleeve_ratio,
|
|
|
|
|
0.10, arm_length - hand_length - cuff_length)
|
|
|
|
|
var bare_length := maxf(0.0,
|
|
|
|
|
arm_length - clothed_length - cuff_length - hand_length)
|
|
|
|
|
|
|
|
|
|
var sleeve := MeshInstance3D.new()
|
|
|
|
|
sleeve.name = "Sleeve"
|
|
|
|
|
sleeve.mesh = _tapered_limb_mesh(
|
|
|
|
|
clothed_length, Vector2(0.068, 0.052), Vector2(0.055, 0.043),
|
|
|
|
|
sleeve_mat)
|
|
|
|
|
_tag_arm_piece(sleeve, "sleeve")
|
|
|
|
|
pivot.add_child(sleeve)
|
|
|
|
|
|
|
|
|
|
var cuff = MeshInstance3D.new()
|
|
|
|
|
var cuff_mesh = BoxMesh.new()
|
|
|
|
|
cuff_mesh.size = Vector3(0.085, 0.085, 0.05)
|
|
|
|
|
cuff_mesh.material = cuff_mat
|
|
|
|
|
cuff.mesh = cuff_mesh
|
|
|
|
|
cuff.position.z = -0.52
|
|
|
|
|
var cuff := MeshInstance3D.new()
|
|
|
|
|
cuff.name = "Cuff"
|
|
|
|
|
cuff.mesh = _tapered_limb_mesh(
|
|
|
|
|
cuff_length, Vector2(0.058, 0.045), Vector2(0.052, 0.040),
|
|
|
|
|
cuff_mat)
|
|
|
|
|
cuff.position.z = -clothed_length
|
|
|
|
|
_tag_arm_piece(cuff, "cuff")
|
|
|
|
|
pivot.add_child(cuff)
|
|
|
|
|
|
|
|
|
|
var hand_box = MeshInstance3D.new()
|
|
|
|
|
var hand_mesh = BoxMesh.new()
|
|
|
|
|
hand_mesh.size = Vector3(0.06, 0.055, 0.12)
|
|
|
|
|
hand_mesh.material = skin_mat
|
|
|
|
|
hand_box.mesh = hand_mesh
|
|
|
|
|
hand_box.position.z = -0.60
|
|
|
|
|
pivot.add_child(hand_box)
|
|
|
|
|
var forearm := MeshInstance3D.new()
|
|
|
|
|
forearm.name = "Forearm"
|
|
|
|
|
forearm.mesh = _tapered_limb_mesh(
|
|
|
|
|
maxf(bare_length, 0.01), Vector2(0.050, 0.039),
|
|
|
|
|
Vector2(0.044, 0.034), skin_mat)
|
|
|
|
|
forearm.position.z = -(clothed_length + cuff_length)
|
|
|
|
|
forearm.visible = bare_length > 0.005
|
|
|
|
|
_tag_arm_piece(forearm, "forearm")
|
|
|
|
|
pivot.add_child(forearm)
|
|
|
|
|
|
|
|
|
|
var hand_root := Node3D.new()
|
|
|
|
|
hand_root.position.z = -(arm_length - hand_length)
|
|
|
|
|
pivot.add_child(hand_root)
|
|
|
|
|
_build_anime_hand(
|
|
|
|
|
hand_root, hand_length, arm_name == "ArmL",
|
|
|
|
|
skin_mat, glove_mat, cuff_mat, nail_mat)
|
|
|
|
|
|
|
|
|
|
weapon.add_child(pivot)
|
|
|
|
|
|
|
|
|
|