Skip to content

Commit

Permalink
Add SnapPath (#685)
Browse files Browse the repository at this point in the history
Add a new snap path control that allows you to snap objects on a rail.
  • Loading branch information
squidt authored Nov 13, 2024
1 parent 6e3eedb commit 6e830bd
Show file tree
Hide file tree
Showing 12 changed files with 405 additions and 38 deletions.
176 changes: 176 additions & 0 deletions addons/godot-xr-tools/objects/snap_path.gd
Original file line number Diff line number Diff line change
@@ -0,0 +1,176 @@
@tool
class_name XRToolsSnapPath
extends XRToolsSnapZone


## An [XRToolsSnapZone] that allows [XRToolsPickable] to be placed along a
## child [Path3D] node. They can either be placed along any point in the curve
## or at discrete intervals by setting "snap_interval" above '0.0'.
##
## Note: Attached [XRToolsPickable]s will face the +Z axis.


## Real world distance between intervals in Meters.
## Enabled when not 0
@export var snap_interval := 0.0:
set(v): snap_interval = absf(v)

@onready var path : Path3D


func _ready() -> void:
super._ready()

for c in get_children():
if c is Path3D:
path = c
break


func has_snap_interval() -> bool:
return !is_equal_approx(snap_interval, 0.0)


func _get_configuration_warnings() -> PackedStringArray:
# Check for Path3D child
for c in get_children():
if c is Path3D:
path = c
return[]
return["This node requires a Path3D child node to define its shape."]


# Called when a target in our grab area is dropped
func _on_target_dropped(target: Node3D) -> void:
# Skip if invalid
if !enabled or !path or !target.can_pick_up(self) or \
!is_instance_valid(target) or \
is_instance_valid(picked_up_object):
return

# Make a zone that will destruct once its object has left
var zone = _make_temp_zone()
var offset = _find_offset(path, target.global_position)

# if snap guide
if _has_snap_guide(target):
# comply with guide
offset = _find_closest_offset_with_length(path.curve, offset, _get_snap_guide(target).length)

# too large to place on path
if is_equal_approx(offset, -1.0):
return

# if snap_interval has been set, use it
if has_snap_interval():
offset = snappedf(offset, snap_interval)

# set position
zone.position = path.curve.sample_baked(offset)

# Add zone as a child
path.add_child(zone)
zone.owner = path

# Connect self-destruct with lambda
zone.has_dropped.connect(func(): zone.queue_free(), Object.ConnectFlags.CONNECT_ONE_SHOT)

# Use Pickable's Shapes as our Shapes
for c in target.get_children():
if c is CollisionShape3D:
PhysicsServer3D.area_add_shape(zone.get_rid(), c.shape.get_rid(), c.transform)

# Force pickup
zone.pick_up_object(target)


# Make a zone that dies on dropping objects
func _make_temp_zone():
var zone = XRToolsSnapZone.new()

# connect lambda to play stash sounds when temp zone picks up
if has_node("AudioStreamPlayer3D"):
zone.has_picked_up.connect(\
func(object):\
$AudioStreamPlayer3D.stream = stash_sound;\
$AudioStreamPlayer3D.play()\
)

# XRToolsSnapZone manaul copy
zone.enabled = true
zone.stash_sound = stash_sound
zone.grab_distance = grab_distance
zone.snap_mode = snap_mode
zone.snap_require = snap_require
zone.snap_exclude = snap_exclude
zone.grab_require = grab_require
zone.grab_exclude = grab_exclude
zone.initial_object = NodePath()

# CollisionObject3D manual copy
zone.disable_mode = disable_mode
zone.collision_layer = collision_layer
zone.collision_mask = collision_mask
zone.collision_priority = collision_priority

return zone


func _has_snap_guide(target: Node3D) -> bool:
for c in target.get_children():
if c is XRToolsSnapPathGuide:
return true
return false


func _get_snap_guide(target: Node3D) -> Node3D:
for c in target.get_children():
if c is XRToolsSnapPathGuide:
return c
return null


# Returns -1 if invalid
# _offset should be in _curve's local coordinates
func _find_closest_offset_with_length(_curve: Curve3D, _offset: float, _length: float) -> float:
# p1 and p2 are the object's start and end respectively
var p1 = _offset
var p2 = _offset - _length

# a _curve's final point is its end, aka the furthest 'forward', which is why it is p1
# path_p1 and path_p2 are the curve's start and end respectively
var path_p1 := _curve.get_closest_offset(_curve.get_point_position(_curve.point_count-1))
var path_p2 := _curve.get_closest_offset(_curve.get_point_position(0))

# if at front (or beyond)
if is_equal_approx(p1, path_p1):
# if too large
if p2 < path_p2:
return -1
# if too far back
elif p2 < path_p2:
# check if snapping will over-extend
if has_snap_interval():
# snapping p1_new may move it further back, and out-of-bounds
# larger snaps move the object further forward
var p1_new = path_p2 + _length
var ideal_snap = snappedf(p1_new, snap_interval)
var more_snap = _snappedf_up(p1_new, snap_interval)
# if ideal snap fits, take that
if ideal_snap >= p1_new:
return ideal_snap
return more_snap
return path_p2 + _length
# otherwise: within bounds
return p1


## Round 'x' upwards to the nearest 'step'
func _snappedf_up(x, step) -> float:
return step * ceilf(x / step)


func _find_offset(_path: Path3D, _global_position: Vector3) -> float:
# transform target pos to local space
var local_pos: Vector3 = _global_position * _path.global_transform
return _path.curve.get_closest_offset(local_pos)
32 changes: 32 additions & 0 deletions addons/godot-xr-tools/objects/snap_path.tscn
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
[gd_scene load_steps=4 format=3 uid="uid://dsstvanwd58r0"]

[ext_resource type="Script" path="res://addons/godot-xr-tools/objects/snap_path.gd" id="1_m211o"]

[sub_resource type="BoxShape3D" id="BoxShape3D_pik8g"]
size = Vector3(0.1, 0.1, 1)

[sub_resource type="Curve3D" id="Curve3D_w68am"]
_data = {
"points": PackedVector3Array(0, 0, 0, 0, 0, 0, 0, 0, -0.5, 0, 0, 0, 0, 0, 0, 0, 0, 0.5),
"tilts": PackedFloat32Array(0, 0)
}
point_count = 2

[node name="SnapPath" type="Area3D"]
collision_layer = 4
collision_mask = 65540
script = ExtResource("1_m211o")

[node name="CollisionShape3D" type="CollisionShape3D" parent="."]
shape = SubResource("BoxShape3D_pik8g")

[node name="AudioStreamPlayer3D" type="AudioStreamPlayer3D" parent="."]
unit_size = 3.0
max_db = 1.0
max_distance = 100.0

[node name="Path3D" type="Path3D" parent="."]
curve = SubResource("Curve3D_w68am")

[connection signal="body_entered" from="." to="." method="_on_snap_zone_body_entered"]
[connection signal="body_exited" from="." to="." method="_on_snap_zone_body_exited"]
14 changes: 14 additions & 0 deletions addons/godot-xr-tools/objects/snap_path_guide.gd
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
@tool
class_name XRToolsSnapPathGuide
extends Marker3D


## XRToolsSnapRailGuide depicts a guide for [XRToolsSnapPath] to judge the
## length of an [XRToolsPickable], helping place pickables within its bounds.
## Add as a child node to any [XRToolsPickable], then move negatively along
## the Z-Axis to define a length.


var length : float:
get:
return abs(position.z)
22 changes: 15 additions & 7 deletions addons/godot-xr-tools/objects/snap_zone.gd
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,14 @@ func is_xr_class(name : String) -> bool:

func _ready():
# Set collision shape radius
$CollisionShape3D.shape.radius = grab_distance
if has_node("CollisionShape3D") and "radius" in $CollisionShape3D.shape:
$CollisionShape3D.shape.radius = grab_distance

# Add important connections
if not body_entered.is_connected(_on_snap_zone_body_entered):
body_entered.connect(_on_snap_zone_body_exited)
if not body_exited.is_connected(_on_snap_zone_body_exited):
body_exited.connect(_on_snap_zone_body_exited)

# Perform updates
_update_snap_mode()
Expand Down Expand Up @@ -244,12 +251,13 @@ func pick_up_object(target: Node3D) -> void:

# Pick up our target. Note, target may do instant drop_and_free
picked_up_object = target
var player = get_node("AudioStreamPlayer3D")
if is_instance_valid(player):
if player.playing:
player.stop()
player.stream = stash_sound
player.play()
if has_node("AudioStreamPlayer3D"):
var player = get_node("AudioStreamPlayer3D")
if is_instance_valid(player):
if player.playing:
player.stop()
player.stream = stash_sound
player.play()

target.pick_up(self)

Expand Down
8 changes: 4 additions & 4 deletions addons/godot-xr-tools/xr/start_xr.gd
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,10 @@ signal xr_ended
signal xr_failed_to_initialize


## XR active flag
static var _xr_active : bool = false


## Optional viewport to control
@export var viewport : Viewport

Expand All @@ -50,13 +54,9 @@ var xr_interface : XRInterface
## XR frame rate
var xr_frame_rate : float = 0


# Is a WebXR is_session_supported query running
var _webxr_session_query : bool = false

## XR active flag
static var _xr_active : bool = false


# Handle auto-initialization when ready
func _ready() -> void:
Expand Down
10 changes: 0 additions & 10 deletions assets/3dmodelscc0/models/scenes/sniper_rifle.gd
Original file line number Diff line number Diff line change
@@ -1,17 +1,7 @@
@tool
extends XRToolsPickable

@export var player_camera : Camera3D:
set(value):
player_camera = value
if is_inside_tree():
_update_player_camera()

func _update_player_camera():
$sniper_rifle/ScopeDisplay.player_camera = player_camera

# Called when the node enters the scene tree for the first time.
func _ready():
super._ready()
_update_player_camera()

23 changes: 9 additions & 14 deletions assets/3dmodelscc0/models/scenes/sniper_rifle.tscn
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@
[ext_resource type="PackedScene" uid="uid://c25yxb0vt53vc" path="res://addons/godot-xr-tools/objects/grab_points/grab_point_hand_left.tscn" id="2_x4cgt"]
[ext_resource type="Animation" uid="uid://ddbo6ioa282en" path="res://addons/godot-xr-tools/hands/animations/left/Pistol.res" id="3_np0oa"]
[ext_resource type="Script" path="res://addons/godot-xr-tools/hands/poses/hand_pose_settings.gd" id="4_mworp"]
[ext_resource type="PackedScene" uid="uid://e8cx22o0aoxa" path="res://assets/3dmodelscc0/models/scenes/scope_display.tscn" id="5_oy4qx"]
[ext_resource type="PackedScene" uid="uid://ctw7nbntd5pcj" path="res://addons/godot-xr-tools/objects/grab_points/grab_point_hand_right.tscn" id="5_sjb82"]
[ext_resource type="PackedScene" uid="uid://da2qgxxwwitl6" path="res://addons/godot-xr-tools/objects/highlight/highlight_ring.tscn" id="6_x8gva"]
[ext_resource type="Script" path="res://assets/digitaln8m4r3/scripts/firearm_slide.gd" id="7_gwebu"]
Expand All @@ -22,9 +21,10 @@
[ext_resource type="PackedScene" uid="uid://dc5t2qgmhb2nf" path="res://addons/godot-xr-tools/objects/hand_pose_area.tscn" id="16_4b70r"]
[ext_resource type="Animation" uid="uid://m5x2m8x3tcel" path="res://addons/godot-xr-tools/hands/animations/left/Pinch Tight.res" id="17_in4xb"]
[ext_resource type="Animation" uid="uid://ca21ej1p3g2yt" path="res://addons/godot-xr-tools/hands/animations/right/Pinch Tight.res" id="18_1meaq"]
[ext_resource type="PackedScene" uid="uid://cxytulli8pgww" path="res://scenes/pickable_demo/objects/picatinny_rail.tscn" id="20_e06no"]

[sub_resource type="BoxShape3D" id="BoxShape3D_apd71"]
size = Vector3(0.0449182, 0.225, 1.145)
size = Vector3(0.0449182, 0.184766, 1.145)

[sub_resource type="SphereShape3D" id="SphereShape3D_exh4i"]
radius = 0.06
Expand Down Expand Up @@ -116,7 +116,7 @@ script = ExtResource("2_bdnea")
second_hand_grab = 2

[node name="CollisionShape3D" parent="." index="0"]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -0.00313599, -0.0294102, 0.0465783)
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -0.00313599, -0.0495274, 0.0465783)
shape = SubResource("BoxShape3D_apd71")

[node name="HighlightRing" parent="." index="1" instance=ExtResource("6_x8gva")]
Expand All @@ -141,6 +141,7 @@ surface_material_override/0 = ExtResource("8_7v2rw")
surface_material_override/0 = ExtResource("8_7v2rw")

[node name="Scope" parent="sniper_rifle/SniperRifle" index="5"]
visible = false
surface_material_override/0 = ExtResource("8_7v2rw")

[node name="Stock" parent="sniper_rifle/SniperRifle" index="6"]
Expand All @@ -149,12 +150,7 @@ surface_material_override/0 = ExtResource("8_7v2rw")
[node name="Trigger" parent="sniper_rifle/SniperRifle" index="7"]
surface_material_override/0 = ExtResource("8_7v2rw")

[node name="ScopeDisplay" parent="sniper_rifle" index="1" instance=ExtResource("5_oy4qx")]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -0.00361723, 0.0610211, 0.324882)
radius = 0.01
fov = 5.0

[node name="FirearmSlide" type="Node3D" parent="sniper_rifle" index="2"]
[node name="FirearmSlide" type="Node3D" parent="sniper_rifle" index="1"]
script = ExtResource("7_gwebu")
slider_end = 0.072

Expand Down Expand Up @@ -240,14 +236,13 @@ drive_position = 0.0
drive_angle = 0.0
drive_aim = 1.0

[node name="MagSnapZone" parent="." index="8" groups=["sniper_slot"] instance=ExtResource("15_coy4c")]
[node name="PicatinnyRail" parent="." index="8" instance=ExtResource("20_e06no")]
transform = Transform3D(-1, 0, 8.74228e-08, 0, 1, 0, -8.74228e-08, 0, -1, -0.00377285, 0.0421098, 0.0647577)

[node name="MagSnapZone" parent="." index="9" groups=["sniper_slot"] instance=ExtResource("15_coy4c")]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -0.00341882, -0.0350358, 0.15841)
snap_require = "sinper_magazine"

[node name="ScopeSnapZone" parent="." index="9" groups=["sniper_attachment"] instance=ExtResource("15_coy4c")]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0.00416867, 0.0566482, 0.159675)
snap_require = "sniper_scope"

[node name="HandPoseArea" parent="." index="10" node_paths=PackedStringArray("grabpoints") instance=ExtResource("16_4b70r")]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -0.00371962, 0.0341488, 0.238741)
left_pose = SubResource("Resource_xtv8j")
Expand Down
Binary file added assets/meshes/picatinny_rail/picatinny_30.glb
Binary file not shown.
Loading

0 comments on commit 6e830bd

Please sign in to comment.