Compare commits

...
Sign in to create a new pull request.

11 commits

Author SHA1 Message Date
Hartmut Seichter
0f925bb1df added icon for asset library 2025-07-08 23:00:28 +02:00
Hartmut Seichter
7987fa3c8a added signalling to button and demo 2025-07-08 21:25:46 +02:00
af27e657f6 rename project 2025-07-08 11:21:37 +02:00
b90b94922e added proper image URL 2025-07-08 09:50:26 +02:00
5b203c9531 refactor work and documentation 2025-07-08 09:45:49 +02:00
46491f8e2f rename to VRPN_Client to make sure there is no confusion 2025-07-08 08:49:36 +02:00
Hartmut Seichter
3f823111d1 make proper setup for VRPN with buttons and trackers 2025-07-07 22:43:04 +02:00
ea953e92a6 try to follow GDScript guidelines for code structure 2025-07-04 22:58:49 +02:00
b54c78da19 better proxying of data into the receivers 2025-07-03 10:27:08 +02:00
Hartmut Seichter
4da8618746 merge refactor branch 2025-07-02 23:34:02 +02:00
Hartmut Seichter
ec4728ed2a update 2025-06-30 22:10:49 +02:00
18 changed files with 339 additions and 61 deletions

View file

@ -2,13 +2,13 @@
A native GDScript implementation of the [VRPN](https://vrpn.github.io/) client protocol.
# TOC
# Background
Various VR tools even today in use are providing an interface for tracking data by implementing a VPRN server. Binding a the actual VRPN library into an extension is proven and working but is quite a burden if the main aim of usage is receiving tracking data. Hence, this project was born to allow for easy integration of VRPN tracking in Godot.
VR hardware and software provide commonly an interface for tracking data by implementing a VPRN server. Binding the actual VRPN client library into an extension is proven and working poses a burden for maintenance and deployability. To solve this gdvrpn allows for a native integration of VRPN tracking in Godot.
This project does not implement any server components and is only tested with a limited number of devices in our lab.
This project does not implement any server components or forwarding and is only tested with a limited number of devices in our lab. Currently, UDP mode is under development.
![Tracking with an OptiTrack System](addons/vrpn/docs/img/gdvrpn-optitrack.jpg)
# Install
@ -16,7 +16,21 @@ Install the project from the AssetLib in Godot or directly from the repository.
# Usage
TBW
There are two components, a `VRPN_Client` and needed for using this addon:
`VPRPN_Client` is mandatory and implements the network component of this addon that parses the data received and broadcasts them to the respective receivers within the scene tree.
`VRPN_Tracker` is a receiver taking pose information (i.e. position and orientation) of a rigid body and applies it to the nodes global transform.
`VRPN_Button` is a receiver of button events.
## Example
For some inspiration try the example `spin_tracker.tscn`. To create a test input use the shipped VRPN config file `test/vrpn.test.cfg` as follows (from the root of this repository)
```bash
$> vrpn_server -f addons/vrpn/tests/vrpn.test.cfg
```
# License

View file

@ -0,0 +1,11 @@
---
---
```sh
[you@yourpc uvrpn]$ vrpn_server -f addons/vrpn/tests/vrpn.test.cfg
Reading from config file addons/vrpn/tests/vrpn.test.cfg
Opening vrpn_Tracker_Spin: Tracker0 with 1 sensors, rate 200.000000
Opening vrpn_Tracker_Spin: Tracker1 with 1 sensors, rate 200.000000
Opening vrpn_Tracker_Spin: Tracker2 with 1 sensors, rate 200.000000
Opening vrpn_Button_Example: Button0 with 2 sensors, toggle rate 2.000000
```

View file

@ -12,12 +12,14 @@ and immediatly made available. Minimal or no internal state keeping!
- first collect names and ids of message_types and senders (stored in session)
- register listeners in session
- listeners inject captured data with signals
- keep naming "classic" ??? ... above session is a tracker with a sender with sensors
# Todo
- [x] Testset for Quaternion rotations
- [x] Axis as subscene
- [ ] make global/local a choice
- [ ] add velocity and acceleration
- [ ] add buttons
# Internal Notes

View file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 MiB

View file

@ -1,7 +1,7 @@
[gd_scene load_steps=5 format=3 uid="uid://en7tpf1d6yak"]
[ext_resource type="Script" uid="uid://dmq3i7qmo1qe0" path="res://addons/vrpn/scripts/VRPN.gd" id="1_jrm7s"]
[ext_resource type="Script" uid="uid://dpj1wrvfsiq4v" path="res://addons/vrpn/scripts/VRPN_Receiver.gd" id="2_fp2uy"]
[ext_resource type="Script" uid="uid://dmq3i7qmo1qe0" path="res://addons/vrpn/scripts/VRPN_Client.gd" id="1_jrm7s"]
[ext_resource type="Script" uid="uid://dpj1wrvfsiq4v" path="res://addons/vrpn/scripts/VRPN_Tracker.gd" id="2_fp2uy"]
[ext_resource type="PackedScene" uid="uid://b426fy7d6jw2d" path="res://addons/vrpn/assets/axis.blend" id="3_73ywu"]
[sub_resource type="PlaneMesh" id="PlaneMesh_24d08"]
@ -19,20 +19,21 @@ shadow_enabled = true
[node name="Root" type="Node3D" parent="."]
[node name="VRPN" type="Node3D" parent="Root" node_paths=PackedStringArray("tracker_receivers")]
[node name="VRPN" type="Node3D" parent="Root"]
script = ExtResource("1_jrm7s")
tracker_receivers = [NodePath("RB1"), NodePath("RB2")]
vrpn_server = "212.201.64.122"
[node name="RB1" type="Node3D" parent="Root/VRPN"]
[node name="RB1" type="Node3D" parent="Root/VRPN" node_paths=PackedStringArray("vrpn_client")]
script = ExtResource("2_fp2uy")
vrpn_client = NodePath("..")
tracker_name = "RB1"
[node name="axis" parent="Root/VRPN/RB1" instance=ExtResource("3_73ywu")]
transform = Transform3D(0.1, 0, 0, 0, 0.1, 0, 0, 0, 0.1, 0, 0, 0)
[node name="RB2" type="Node3D" parent="Root/VRPN"]
[node name="RB2" type="Node3D" parent="Root/VRPN" node_paths=PackedStringArray("vrpn_client")]
script = ExtResource("2_fp2uy")
vrpn_client = NodePath("..")
tracker_name = "RB2"
[node name="axis" parent="Root/VRPN/RB2" instance=ExtResource("3_73ywu")]

View file

@ -1,12 +1,29 @@
[gd_scene load_steps=5 format=3 uid="uid://bj5ykdjle10tt"]
[gd_scene load_steps=8 format=3 uid="uid://bj5ykdjle10tt"]
[ext_resource type="Script" uid="uid://dmq3i7qmo1qe0" path="res://addons/vrpn/scripts/VRPN.gd" id="2_24d08"]
[ext_resource type="Script" uid="uid://dpj1wrvfsiq4v" path="res://addons/vrpn/scripts/VRPN_Receiver.gd" id="2_170dk"]
[ext_resource type="Script" uid="uid://dmq3i7qmo1qe0" path="res://addons/vrpn/scripts/VRPN_Client.gd" id="2_24d08"]
[ext_resource type="Script" uid="uid://dpj1wrvfsiq4v" path="res://addons/vrpn/scripts/VRPN_Tracker.gd" id="2_170dk"]
[ext_resource type="PackedScene" uid="uid://b426fy7d6jw2d" path="res://addons/vrpn/assets/axis.blend" id="3_170dk"]
[ext_resource type="Script" uid="uid://bmlyip5xa5df4" path="res://addons/vrpn/scripts/VRPN_Button.gd" id="4_j4l28"]
[sub_resource type="PlaneMesh" id="PlaneMesh_24d08"]
size = Vector2(6, 2)
[sub_resource type="SphereMesh" id="SphereMesh_j4l28"]
radius = 0.25
height = 0.5
radial_segments = 32
rings = 16
[sub_resource type="GDScript" id="GDScript_j4l28"]
resource_name = "ButtonReact"
script/source = "extends Node3D
func _on_button_0_on_vrpn_button(data):
if 0 in data['changes']:
self.visible = (data['changes'][0] == 1)
"
[node name="Node3D" type="Node3D"]
[node name="Camera3D" type="Camera3D" parent="."]
@ -18,9 +35,8 @@ shadow_enabled = true
[node name="Root" type="Node3D" parent="."]
[node name="VRPN" type="Node3D" parent="Root" node_paths=PackedStringArray("tracker_receivers")]
[node name="VRPN" type="Node3D" parent="Root"]
script = ExtResource("2_24d08")
tracker_receivers = [NodePath("../SpinTracker/Offset0/Tracker0"), NodePath("../SpinTracker/Tracker1"), NodePath("../SpinTracker/Offset2/Tracker2")]
[node name="Floor" type="MeshInstance3D" parent="Root"]
mesh = SubResource("PlaneMesh_24d08")
@ -28,8 +44,9 @@ mesh = SubResource("PlaneMesh_24d08")
[node name="SpinTracker" type="Node3D" parent="Root"]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0.764802, 0)
[node name="Tracker1" type="Node3D" parent="Root/SpinTracker"]
[node name="Tracker1" type="Node3D" parent="Root/SpinTracker" node_paths=PackedStringArray("vrpn_client")]
script = ExtResource("2_170dk")
vrpn_client = NodePath("../../VRPN")
tracker_name = "Tracker1"
tracker_use_position = false
@ -43,8 +60,9 @@ text = "Y-Axis"
[node name="Offset2" type="Node3D" parent="Root/SpinTracker"]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 2, 0, 0)
[node name="Tracker2" type="Node3D" parent="Root/SpinTracker/Offset2"]
[node name="Tracker2" type="Node3D" parent="Root/SpinTracker/Offset2" node_paths=PackedStringArray("vrpn_client")]
script = ExtResource("2_170dk")
vrpn_client = NodePath("../../../VRPN")
tracker_name = "Tracker2"
tracker_use_position = false
@ -58,8 +76,9 @@ text = "Z-Axis"
[node name="Offset0" type="Node3D" parent="Root/SpinTracker"]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -2, 0, 0)
[node name="Tracker0" type="Node3D" parent="Root/SpinTracker/Offset0"]
[node name="Tracker0" type="Node3D" parent="Root/SpinTracker/Offset0" node_paths=PackedStringArray("vrpn_client")]
script = ExtResource("2_170dk")
vrpn_client = NodePath("../../../VRPN")
tracker_use_position = false
[node name="axis" parent="Root/SpinTracker/Offset0/Tracker0" instance=ExtResource("3_170dk")]
@ -68,3 +87,18 @@ transform = Transform3D(0.1, 0, 0, 0, 0.1, 0, 0, 0, 0.1, 0, 0, 0)
[node name="Label3D" type="Label3D" parent="Root/SpinTracker/Offset0/Tracker0"]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0.769847, 0)
text = "X-Axis"
[node name="Button0" type="Node3D" parent="Root/SpinTracker" node_paths=PackedStringArray("vrpn_client")]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 1.21693, 0)
script = ExtResource("4_j4l28")
vrpn_client = NodePath("../../VRPN")
[node name="MeshInstance3D" type="MeshInstance3D" parent="Root/SpinTracker/Button0"]
mesh = SubResource("SphereMesh_j4l28")
script = SubResource("GDScript_j4l28")
[node name="Label3D" type="Label3D" parent="Root/SpinTracker/Button0/MeshInstance3D"]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0.431905, 0)
text = "Button"
[connection signal="on_vrpn_button" from="Root/SpinTracker/Button0" to="Root/SpinTracker/Button0/MeshInstance3D" method="_on_button_0_on_vrpn_button"]

BIN
addons/vrpn/icons/icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.5 KiB

View file

@ -0,0 +1,34 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://b6ryn3e6ymnyj"
path="res://.godot/imported/icon.png-57fdb6924c90abbcc87b543a14f2061e.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://addons/vrpn/icons/icon.png"
dest_files=["res://.godot/imported/icon.png-57fdb6924c90abbcc87b543a14f2061e.ctex"]
[params]
compress/mode=0
compress/high_quality=false
compress/lossy_quality=0.7
compress/hdr_compression=1
compress/normal_map=0
compress/channel_pack=0
mipmaps/generate=false
mipmaps/limit=-1
roughness/mode=0
roughness/src_normal=""
process/fix_alpha_border=true
process/premult_alpha=false
process/normal_map_invert_y=false
process/hdr_as_srgb=false
process/hdr_clamp_exposure=false
process/size_limit=0
detect_3d/compress_to=1

View file

@ -1 +1,89 @@
<svg xmlns="http://www.w3.org/2000/svg" width="128" height="128"><rect width="124" height="124" x="2" y="2" fill="#363d52" stroke="#212532" stroke-width="4" rx="14"/><g fill="#fff" transform="translate(12.322 12.322)scale(.101)"><path d="M105 673v33q407 354 814 0v-33z"/><path fill="#478cbf" d="m105 673 152 14q12 1 15 14l4 67 132 10 8-61q2-11 15-15h162q13 4 15 15l8 61 132-10 4-67q3-13 15-14l152-14V427q30-39 56-81-35-59-83-108-43 20-82 47-40-37-88-64 7-51 8-102-59-28-123-42-26 43-46 89-49-7-98 0-20-46-46-89-64 14-123 42 1 51 8 102-48 27-88 64-39-27-82-47-48 49-83 108 26 42 56 81zm0 33v39c0 276 813 276 814 0v-39l-134 12-5 69q-2 10-14 13l-162 11q-12 0-16-11l-10-65H446l-10 65q-4 11-16 11l-162-11q-12-3-14-13l-5-69z"/><path d="M483 600c0 34 58 34 58 0v-86c0-34-58-34-58 0z"/><circle cx="725" cy="526" r="90"/><circle cx="299" cy="526" r="90"/></g><g fill="#414042" transform="translate(12.322 12.322)scale(.101)"><circle cx="307" cy="532" r="60"/><circle cx="717" cy="532" r="60"/></g></svg>
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
width="128"
height="128"
version="1.1"
id="svg6"
sodipodi:docname="icon.svg"
inkscape:version="1.4.2 (ebf0e940d0, 2025-05-08)"
inkscape:export-filename="icon.png"
inkscape:export-xdpi="96"
inkscape:export-ydpi="96"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<path
style="font-weight:bold;font-size:16px;line-height:1.25;font-family:'Fira Sans';-inkscape-font-specification:'Fira Sans, Normal';text-align:end;letter-spacing:0px;word-spacing:0px;text-anchor:end;stroke-width:2.60642"
d="M 36.466327,83.172862 H 29.46027 L 24.289133,106.7349 18.784374,83.172862 h -7.214571 l 8.340544,28.899988 h 8.507355 z M 54.02316,112.07285 h 7.715003 L 54.398484,99.770545 c 3.753245,-1.709812 5.546462,-4.003461 5.546462,-7.798409 0,-6.005192 -4.045164,-8.799274 -11.885276,-8.799274 h -9.466517 v 28.899988 h 6.839246 v -11.05122 h 2.668974 z M 45.432399,87.968675 h 2.46046 c 3.377921,0 5.004327,1.209379 5.004327,4.003461 0,3.044299 -1.584703,4.337083 -4.5873,4.337083 h -2.877487 z m 28.232766,-4.795813 h -9.341409 v 28.899988 h 6.839246 v -9.63333 h 3.127704 c 6.672436,0 11.426546,-3.127705 11.426546,-9.841843 0,-6.338814 -4.5873,-9.424815 -12.052087,-9.424815 z m 0.0417,14.345736 H 71.162999 V 88.05208 h 2.418758 c 3.37792,0 5.087732,1.459596 5.087732,4.545597 0,3.628137 -1.876623,4.920921 -4.962624,4.920921 z M 111.61466,83.172862 h -6.1303 v 11.676762 c 0,4.629002 0.54213,8.423946 0.91746,10.759306 L 97.852759,83.172862 h -8.507355 v 28.899988 h 6.088597 v -10.59249 c 0,-5.337952 -0.542135,-9.174602 -0.875757,-11.801874 l 8.340546,22.394364 h 8.71587 z"
id="text8"
aria-label="VRPN" />
<defs
id="defs6">
<marker
style="overflow:visible"
id="Triangle"
refX="0"
refY="0"
orient="auto-start-reverse"
inkscape:stockid="Triangle arrow"
markerWidth="1"
markerHeight="1"
viewBox="0 0 1 1"
inkscape:isstock="true"
inkscape:collect="always"
preserveAspectRatio="xMidYMid">
<path
transform="scale(0.5)"
style="fill:context-stroke;fill-rule:evenodd;stroke:context-stroke;stroke-width:1pt"
d="M 5.77,0 -2.88,5 V -5 Z"
id="path135" />
</marker>
</defs>
<sodipodi:namedview
id="namedview6"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:showpageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1"
showguides="false"
inkscape:zoom="5.1983397"
inkscape:cx="15.774267"
inkscape:cy="37.896715"
inkscape:window-width="2914"
inkscape:window-height="1477"
inkscape:window-x="26"
inkscape:window-y="23"
inkscape:window-maximized="0"
inkscape:current-layer="svg6" />
<rect
width="124"
height="124"
x="2"
y="2"
fill="#363d52"
stroke="#212532"
stroke-width="4"
rx="14"
id="rect1"
style="fill:none" />
<g
id="g8"
transform="matrix(0.55851618,0,0,0.55851618,28.571682,9.9203741)">
<path
style="fill:none;stroke:#26a269;stroke-width:4;stroke-linecap:butt;stroke-linejoin:miter;stroke-dasharray:none;stroke-opacity:1;marker-end:url(#Triangle)"
d="M 63.5,70.214727 V 23.084294"
id="path6" />
<path
style="fill:none;stroke:#e01b24;stroke-width:4;stroke-linecap:butt;stroke-linejoin:miter;stroke-dasharray:none;stroke-opacity:1;marker-end:url(#Triangle)"
d="m 65,73.460171 40.81615,23.565217"
id="path7" />
<path
style="fill:none;stroke:#1a5fb4;stroke-width:4;stroke-linecap:butt;stroke-linejoin:miter;stroke-dasharray:none;stroke-opacity:1;marker-end:url(#Triangle)"
d="M 61.865858,73.460175 21.04971,97.025388"
id="path8" />
</g>
</svg>

Before

Width:  |  Height:  |  Size: 994 B

After

Width:  |  Height:  |  Size: 3.9 KiB

Before After
Before After

View file

@ -0,0 +1,20 @@
class_name VRPN_Button
extends Node
@export var vrpn_client : VRPN_Client = null
@export var button_sensor : String = "Button0"
@export var register_on_ready : bool = true
var state : int = 0
signal on_vrpn_button(data:Dictionary)
func _ready():
if not vrpn_client:
push_warning("No VRPN client for button on '%s' given." % [self.name])
elif register_on_ready:
vrpn_client.buttons.append(self)
func _on_vrpn(vrpn_data : Dictionary) -> void:
if vrpn_data['sensor'] == button_sensor:
on_vrpn_button.emit(vrpn_data)

View file

@ -0,0 +1 @@
uid://bmlyip5xa5df4

View file

@ -1,8 +1,9 @@
class_name VRPN_Client
extends Node
class_name VRPN
# tracking associated data
enum TrackingData { POS_QUAT, VELOCITY, ACCELERATION }
enum TrackingDataType { POS_QUAT, VELOCITY, ACCELERATION }
enum ButtonDataType { BUTTON_STATE, BUTTON_CHANGE }
# magic cookie
const magic_cookie_start : String = "vrpn: ver."
@ -18,14 +19,14 @@ signal data(data:Array)
signal disconnected
signal error(msg:String)
@onready var _stream: StreamPeerTCP = StreamPeerTCP.new()
@export var tracker_receivers : Array[VRPN_Receiver] = []
@export var vrpn_server : String = "127.0.0.1"
@export var vrpn_port : int = 3883
var tracker : Array[VRPN_Tracker] = []
var buttons : Array[VRPN_Button] = []
@onready var _stream: StreamPeerTCP = StreamPeerTCP.new()
func _ready() -> void:
self.connect_to_host(vrpn_server,vrpn_port)
@ -87,11 +88,11 @@ func _on_data(data : Array):
var as_cookie = bytes.get_string_from_ascii()
# Cookie Hack!
if as_cookie.begins_with(VRPN.magic_cookie_start): #
if as_cookie.begins_with(VRPN_Client.magic_cookie_start): #
# kaboom we just send back the same cookie :)
self.send(bytes)
else:
VRPN.marshall_block(bytes,self)
VRPN_Client.marshall_block(bytes,self)
func _on_connected(s : StreamPeerTCP):
@ -104,7 +105,7 @@ func _on_error(msg:String):
push_warning(msg) # Replace with function body.
static func marshall_block(data : PackedByteArray,session : VRPN) -> void:
static func marshall_block(data : PackedByteArray,client : VRPN_Client) -> void:
# need to fix that
var block_offset : int = 0
@ -133,7 +134,7 @@ static func marshall_block(data : PackedByteArray,session : VRPN) -> void:
#print("message_type '%d'" % message_type)
#print("sequence_num '%d'" % sequence_num)
marshall_body(data.slice(block_offset+header_size,block_offset+length),message_type,sender_id,session)
marshall_body(data.slice(block_offset+header_size,block_offset+length),message_type,sender_id,client)
# next datablock
block_offset += aligned_size(length)
@ -143,7 +144,7 @@ static func decode_string(stream : StreamPeerBuffer) -> String:
return stream.get_string(len)
static func marshall_body(data : PackedByteArray,message_type : int, sender_id: int, session : VRPN):
static func marshall_body(data : PackedByteArray,message_type : int, sender_id: int, client : VRPN_Client):
var body := StreamPeerBuffer.new()
body.data_array = data
body.big_endian = true
@ -156,16 +157,16 @@ static func marshall_body(data : PackedByteArray,message_type : int, sender_id:
-1: # sensor names
var name = decode_string(body)
print("sensor name is '%s' with '%d" % [name,sender_id])
session.sensors[sender_id] = name
client.sensors[sender_id] = name
-2: # message names
var name = decode_string(body)
print("message name is '%s' for message_type '%d'" % [name,sender_id])
session.messages[sender_id] = name
client.messages[sender_id] = name
return
# now we use the string identifiers
# because they are supposedly dynamically assigned
match session.messages[message_type]:
match client.messages[message_type]:
'vrpn_Tracker Pos_Quat': # quat pos
# get id
var sensor_id = body.get_32()
@ -175,19 +176,73 @@ static func marshall_body(data : PackedByteArray,message_type : int, sender_id:
# VRPN quat layout and Godot Quaternion c'tor identical with x,y,z,w
var quat = Quaternion(body.get_double(),body.get_double(),body.get_double(),body.get_double()).normalized()
# submit to listener
for r in session.tracker_receivers:
r._on_pos_quat({
"tracker" : session.sensors[sender_id],
for r in client.tracker:
r._on_vrpn({
"type" : TrackingDataType.POS_QUAT,
"tracker" : client.sensors[sender_id],
"sensor" : sensor_id,
"position" : pos,
"rotation" : quat
})
'vrpn_Tracker Velocity':
pass
# get id
var sensor_id = body.get_32()
var padding = body.get_32() # padding
# position
var vel = Vector3(body.get_double(),body.get_double(),body.get_double())
# VRPN quat layout and Godot Quaternion c'tor identical with x,y,z,w
var vel_rot = Quaternion(body.get_double(),body.get_double(),body.get_double(),body.get_double()).normalized()
# submit to listener
for r in client.tracker:
r._on_vrpn({
"type" : TrackingDataType.VELOCITY,
"tracker" : client.sensors[sender_id],
"sensor" : sensor_id,
"velocity_linear" : vel,
"velocity_rotation" : vel_rot
})
'vrpn_Tracker Acceleration':
# get id
var sensor_id = body.get_32()
var padding = body.get_32() # padding
# position
var acc = Vector3(body.get_double(),body.get_double(),body.get_double())
# VRPN quat layout and Godot Quaternion c'tor identical with x,y,z,w
var acc_rot = Quaternion(body.get_double(),body.get_double(),body.get_double(),body.get_double()).normalized()
var acc_dt = body.get_double() # padding
# submit to listener
for r in client.tracker:
r._on_vrpn({
"type" : TrackingDataType.ACCELERATION,
"tracker" : client.sensors[sender_id],
"sensor" : sensor_id,
"acceleration_linear" : acc,
"acceleration_rotation" : acc_rot,
"acceleration_dt" : acc_dt
})
'vrpn_Button Change':
var num_buttons : int = body.get_32()
var button_changes : Dictionary = {}
for i in range(num_buttons):
button_changes[i] = body.get_32()
for button in client.buttons:
button._on_vrpn(
{
"type" : ButtonDataType.BUTTON_CHANGE,
"sensor" : client.sensors[sender_id],
"changes" : button_changes
}
)
'vrpn_Button States':
var num_buttons : int = body.get_32()
for i in range(num_buttons):
var button_state = body.get_32()
#print("button state {0} : {1}".format([i,button_state]))
_:
pass
#print("unhandled message type {0}".format([message_type]))
static func aligned_size(actual_size : int, alignment : int = 8) -> int:
return (actual_size + alignment - 1) & ~(alignment - 1)

View file

@ -1,18 +0,0 @@
extends Node3D
class_name VRPN_Receiver
@export var tracker_name : String = "Tracker0"
@export var tracker_sensor : int = 0
@export var tracker_use_position : bool = true
@export var tracker_use_rotation : bool = true
func _on_pos_quat(tracker_data : Dictionary):
if tracker_data['tracker'] == tracker_name and tracker_data['sensor'] == tracker_sensor:
if tracker_use_position:
self.global_position = tracker_data['position']
if tracker_use_rotation:
var rotation := tracker_data['rotation'] as Quaternion
self.global_basis = Basis(rotation)

View file

@ -0,0 +1,32 @@
class_name VRPN_Tracker
extends Node3D
@export var vrpn_client : VRPN_Client = null
@export var tracker_name : String = "Tracker0"
@export var tracker_sensor : int = 0
@export var tracker_use_position : bool = true
@export var tracker_use_rotation : bool = true
@export var register_on_ready : bool = true
func _ready() -> void:
if vrpn_client and register_on_ready:
vrpn_client.tracker.append(self)
func _on_vrpn(vrpn_data : Dictionary):
match vrpn_data['type'] as VRPN_Client.TrackingDataType:
VRPN_Client.TrackingDataType.POS_QUAT:
if vrpn_data['tracker'] == tracker_name and vrpn_data['sensor'] == tracker_sensor:
if tracker_use_position:
self.global_position = vrpn_data['position']
if tracker_use_rotation:
var rotation := vrpn_data['rotation'] as Quaternion
self.global_basis = Basis(rotation)
VRPN_Client.TrackingDataType.ACCELERATION:
pass
VRPN_Client.TrackingDataType.VELOCITY:
pass
_:
push_warning("unknown tracker datatype")
func _on_vrpn_connected(s):
pass # Replace with function body.

View file

@ -10,7 +10,11 @@ config_version=5
[application]
config/name="uvrpn"
config/name="gdvrpn"
run/main_scene="uid://bj5ykdjle10tt"
config/features=PackedStringArray("4.4", "Forward Plus")
config/icon="uid://cdprcmtx102rp"
[rendering]
anti_aliasing/quality/screen_space_aa=1