merge refactor branch
This commit is contained in:
commit
4da8618746
23 changed files with 455 additions and 256 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -1,3 +1,4 @@
|
||||||
# Godot 4+ specific ignores
|
# Godot 4+ specific ignores
|
||||||
.godot/
|
.godot/
|
||||||
/android/
|
/android/
|
||||||
|
*.blend?
|
||||||
|
|
9
LICENSE.md
Normal file
9
LICENSE.md
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
|
||||||
|
|
||||||
|
Copyright 2025 Hartmut Seichter
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
18
Notes.md
18
Notes.md
|
@ -1,18 +0,0 @@
|
||||||
# Resources
|
|
||||||
|
|
||||||
- [vrpn-rs description of VRPN protocol](https://github.com/vrpn/vrpn-rs/blob/main/Protocol.md)
|
|
||||||
|
|
||||||
|
|
||||||
# Design
|
|
||||||
|
|
||||||
Allow for a session based design - data needs to be collected per block
|
|
||||||
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
|
|
||||||
|
|
||||||
|
|
23
README.md
Normal file
23
README.md
Normal file
|
@ -0,0 +1,23 @@
|
||||||
|
# gdvrpn
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
|
This project does not implement any server components and is only tested with a limited number of devices in our lab.
|
||||||
|
|
||||||
|
# Install
|
||||||
|
|
||||||
|
Install the project from the AssetLib in Godot or directly from the repository.
|
||||||
|
|
||||||
|
# Usage
|
||||||
|
|
||||||
|
TBW
|
||||||
|
|
||||||
|
# License
|
||||||
|
|
||||||
|
This project is licensed under the terms of the [MIT License](https://opensource.org/license/mit)
|
12
Root.tscn
12
Root.tscn
|
@ -1,12 +0,0 @@
|
||||||
[gd_scene load_steps=2 format=3 uid="uid://bj5ykdjle10tt"]
|
|
||||||
|
|
||||||
[ext_resource type="Script" uid="uid://vnywsf0rn1ax" path="res://SocketClient.gd" id="1_gxo8o"]
|
|
||||||
|
|
||||||
[node name="Node3D" type="Node3D"]
|
|
||||||
script = ExtResource("1_gxo8o")
|
|
||||||
vrpn_server = "212.201.64.122"
|
|
||||||
|
|
||||||
[connection signal="connected" from="." to="." method="_on_connected"]
|
|
||||||
[connection signal="data" from="." to="." method="_on_data"]
|
|
||||||
[connection signal="disconnected" from="." to="." method="_on_disconnected"]
|
|
||||||
[connection signal="error" from="." to="." method="_on_error"]
|
|
|
@ -1,82 +0,0 @@
|
||||||
extends Node
|
|
||||||
|
|
||||||
signal connected(s:StreamPeerTCP)
|
|
||||||
signal data(data:Array)
|
|
||||||
signal disconnected
|
|
||||||
signal error
|
|
||||||
|
|
||||||
|
|
||||||
@onready var _stream: StreamPeerTCP = StreamPeerTCP.new()
|
|
||||||
|
|
||||||
@export var vrpn_server : String = "localhost"
|
|
||||||
@export var vrpn_port : int = 3883
|
|
||||||
|
|
||||||
func _ready() -> void:
|
|
||||||
self.connect_to_host(vrpn_server,vrpn_port)
|
|
||||||
|
|
||||||
func _process(delta: float) -> void:
|
|
||||||
var old_status = _stream.get_status()
|
|
||||||
_stream.poll()
|
|
||||||
var new_status = _stream.get_status()
|
|
||||||
if old_status != new_status:
|
|
||||||
match new_status:
|
|
||||||
_stream.STATUS_NONE:
|
|
||||||
emit_signal("disconnected")
|
|
||||||
_stream.STATUS_CONNECTING:
|
|
||||||
print("Connecting.")
|
|
||||||
_stream.STATUS_CONNECTED:
|
|
||||||
print("Connected.")
|
|
||||||
emit_signal("connected",_stream.poll()
|
|
||||||
)
|
|
||||||
_stream.STATUS_ERROR:
|
|
||||||
print("Error with socket stream.")
|
|
||||||
emit_signal("error")
|
|
||||||
|
|
||||||
if new_status == _stream.STATUS_CONNECTED:
|
|
||||||
var available_bytes: int = _stream.get_available_bytes()
|
|
||||||
if available_bytes > 0:
|
|
||||||
var res = _stream.get_partial_data(available_bytes)
|
|
||||||
if res[0] != OK:
|
|
||||||
emit_signal("error")
|
|
||||||
else:
|
|
||||||
emit_signal("data", res[1])
|
|
||||||
|
|
||||||
func connect_to_host(host: String, port: int) -> void:
|
|
||||||
print("Connecting to %s:%d" % [host, port])
|
|
||||||
if _stream.connect_to_host(host, port) != OK:
|
|
||||||
print("Error connecting to host.")
|
|
||||||
emit_signal("error")
|
|
||||||
|
|
||||||
func send(data: PackedByteArray) -> bool:
|
|
||||||
if _stream.get_status() != _stream.STATUS_CONNECTED:
|
|
||||||
print("Error: Stream is not currently connected.")
|
|
||||||
return false
|
|
||||||
var error: int = _stream.put_data(data)
|
|
||||||
if error != OK:
|
|
||||||
print("Error writing to stream: ", error)
|
|
||||||
return false
|
|
||||||
return true
|
|
||||||
|
|
||||||
|
|
||||||
func _on_data(data : Array):
|
|
||||||
var bytes = PackedByteArray(data)
|
|
||||||
var as_cookie = bytes.get_string_from_ascii()
|
|
||||||
|
|
||||||
# Cookie Hack!
|
|
||||||
if as_cookie.begins_with(VRPN.magic_cookie_start): #
|
|
||||||
# kaboom we just send back the same cookie :)
|
|
||||||
self.send(bytes)
|
|
||||||
else:
|
|
||||||
VRPN.marshall_block(bytes)
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
func _on_connected(s : StreamPeerTCP):
|
|
||||||
print("Connected to",s.get_connected_host()) # Replace with function body.
|
|
||||||
|
|
||||||
func _on_disconnected():
|
|
||||||
print("Disconnected") # Replace with function body.
|
|
||||||
|
|
||||||
func _on_error():
|
|
||||||
print("Error") # Replace with function body.
|
|
||||||
|
|
|
@ -1 +0,0 @@
|
||||||
uid://vnywsf0rn1ax
|
|
87
VRPN.gd
87
VRPN.gd
|
@ -1,87 +0,0 @@
|
||||||
extends Node
|
|
||||||
|
|
||||||
class_name VRPN
|
|
||||||
|
|
||||||
const magic_cookie_start : String = "vrpn: ver."
|
|
||||||
|
|
||||||
static func marshall_block(data : PackedByteArray) -> void:
|
|
||||||
|
|
||||||
# need to fix that
|
|
||||||
var block_offset : int = 0
|
|
||||||
var header_size = aligned_size(20) # kinda redundant as we take the seq number as well
|
|
||||||
|
|
||||||
while data.size() > block_offset:
|
|
||||||
# reader for stream
|
|
||||||
var header := StreamPeerBuffer.new()
|
|
||||||
# get block addresses
|
|
||||||
header.data_array = data.slice(block_offset,block_offset+header_size)
|
|
||||||
# make sure we read as big endian
|
|
||||||
header.big_endian = true
|
|
||||||
|
|
||||||
# read header
|
|
||||||
var length := header.get_32() as int # length of message
|
|
||||||
var time_sec := header.get_32() as int # datetime sec
|
|
||||||
var time_msec := header.get_32() as int # datetime micro sec
|
|
||||||
var sender_id := header.get_32() as int # sender id
|
|
||||||
var message_type := header.get_32() as int # type of message (payload)
|
|
||||||
var sequence_num := header.get_32() as int # inofficial sequence number (padding)
|
|
||||||
|
|
||||||
if false:
|
|
||||||
print("length '%d'" % length)
|
|
||||||
print("time_sec '%d'" % time_sec)
|
|
||||||
print("time_msec '%d'" % time_msec)
|
|
||||||
print("sender_id '%d'" % sender_id)
|
|
||||||
print("message_type '%d'" % message_type)
|
|
||||||
print("sequence_num '%d'" % sequence_num)
|
|
||||||
|
|
||||||
# print
|
|
||||||
|
|
||||||
print("sender_id:{0} message_type:{1} block_offset:{2} header_size:{3} length:{4}".format([sender_id, message_type, block_offset,header_size,length]))
|
|
||||||
|
|
||||||
# directly hand over marshalling for body
|
|
||||||
marshall_body(data.slice(block_offset+header_size,block_offset+length),message_type)
|
|
||||||
|
|
||||||
# next datablock
|
|
||||||
block_offset += aligned_size(length)
|
|
||||||
|
|
||||||
|
|
||||||
static func marshall_body(data : PackedByteArray,message_type : int):
|
|
||||||
var body := StreamPeerBuffer.new()
|
|
||||||
body.data_array = data
|
|
||||||
body.big_endian = true
|
|
||||||
# sender description
|
|
||||||
match message_type: # message_type_ids are supposedly dynamic
|
|
||||||
-1,-2:
|
|
||||||
# get length of string
|
|
||||||
var body_length = body.get_32()
|
|
||||||
# get string
|
|
||||||
var sender_name = body.get_string(body_length)
|
|
||||||
print("sender name is '%s'" % sender_name)
|
|
||||||
#print(body.data_array)
|
|
||||||
4: # quat pos
|
|
||||||
# get id
|
|
||||||
var sensor_id = body.get_32()
|
|
||||||
var padding = body.get_32()
|
|
||||||
var pos = Vector3(body.get_double(),body.get_double(),body.get_double())
|
|
||||||
# VRPN quaternions are w,xyz
|
|
||||||
var quat_w = body.get_double()
|
|
||||||
var quat_x = body.get_double()
|
|
||||||
var quat_y = body.get_double()
|
|
||||||
var quat_z = body.get_double()
|
|
||||||
var quat = Quaternion(quat_x,quat_y,quat_z,quat_w)
|
|
||||||
#print("sensor id {0} {1} {2}".format([sensor_id,pos,quat]))
|
|
||||||
18:
|
|
||||||
var num_buttons = body.get_32()
|
|
||||||
var buttons = {}
|
|
||||||
for i in num_buttons:
|
|
||||||
var button_id = body.get_32()
|
|
||||||
var button_state = body.get_32()
|
|
||||||
buttons[button_id] = button_state
|
|
||||||
|
|
||||||
#print("sensor with {0} buttons".format([buttons]))
|
|
||||||
_:
|
|
||||||
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)
|
|
BIN
addons/vrpn/assets/axis.blend
Normal file
BIN
addons/vrpn/assets/axis.blend
Normal file
Binary file not shown.
62
addons/vrpn/assets/axis.blend.import
Normal file
62
addons/vrpn/assets/axis.blend.import
Normal file
|
@ -0,0 +1,62 @@
|
||||||
|
[remap]
|
||||||
|
|
||||||
|
importer="scene"
|
||||||
|
importer_version=1
|
||||||
|
type="PackedScene"
|
||||||
|
uid="uid://b426fy7d6jw2d"
|
||||||
|
path="res://.godot/imported/axis.blend-64f5b0cb53b3b69c5a0ce687708e9ae2.scn"
|
||||||
|
|
||||||
|
[deps]
|
||||||
|
|
||||||
|
source_file="res://addons/vrpn/assets/axis.blend"
|
||||||
|
dest_files=["res://.godot/imported/axis.blend-64f5b0cb53b3b69c5a0ce687708e9ae2.scn"]
|
||||||
|
|
||||||
|
[params]
|
||||||
|
|
||||||
|
nodes/root_type=""
|
||||||
|
nodes/root_name=""
|
||||||
|
nodes/apply_root_scale=true
|
||||||
|
nodes/root_scale=1.0
|
||||||
|
nodes/import_as_skeleton_bones=false
|
||||||
|
nodes/use_node_type_suffixes=true
|
||||||
|
meshes/ensure_tangents=true
|
||||||
|
meshes/generate_lods=true
|
||||||
|
meshes/create_shadow_meshes=true
|
||||||
|
meshes/light_baking=1
|
||||||
|
meshes/lightmap_texel_size=0.2
|
||||||
|
meshes/force_disable_compression=false
|
||||||
|
skins/use_named_skins=true
|
||||||
|
animation/import=true
|
||||||
|
animation/fps=30
|
||||||
|
animation/trimming=false
|
||||||
|
animation/remove_immutable_tracks=true
|
||||||
|
animation/import_rest_as_RESET=false
|
||||||
|
import_script/path=""
|
||||||
|
_subresources={
|
||||||
|
"nodes": {
|
||||||
|
"PATH:Camera": {
|
||||||
|
"import/skip_import": true
|
||||||
|
},
|
||||||
|
"PATH:Light": {
|
||||||
|
"import/skip_import": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
blender/nodes/visible=0
|
||||||
|
blender/nodes/active_collection_only=false
|
||||||
|
blender/nodes/punctual_lights=true
|
||||||
|
blender/nodes/cameras=true
|
||||||
|
blender/nodes/custom_properties=true
|
||||||
|
blender/nodes/modifiers=1
|
||||||
|
blender/meshes/colors=false
|
||||||
|
blender/meshes/uvs=true
|
||||||
|
blender/meshes/normals=true
|
||||||
|
blender/meshes/export_geometry_nodes_instances=false
|
||||||
|
blender/meshes/tangents=true
|
||||||
|
blender/meshes/skins=2
|
||||||
|
blender/meshes/export_bones_deforming_mesh_only=false
|
||||||
|
blender/materials/unpack_enabled=true
|
||||||
|
blender/materials/export_materials=1
|
||||||
|
blender/animation/limit_playback=true
|
||||||
|
blender/animation/always_sample=true
|
||||||
|
blender/animation/group_tracks=true
|
24
addons/vrpn/docs/Notes.md
Normal file
24
addons/vrpn/docs/Notes.md
Normal file
|
@ -0,0 +1,24 @@
|
||||||
|
# Resources
|
||||||
|
|
||||||
|
Reference for implementing this library are from here [vrpn-rs description of VRPN protocol](https://github.com/vrpn/vrpn-rs/blob/main/Protocol.md)
|
||||||
|
Unfortunately, there are some mistakes in the document, most notably it presumes that
|
||||||
|
Quaternion in VRPN (quat library) are w,x,y,z - which is incorrect.
|
||||||
|
|
||||||
|
# Design
|
||||||
|
|
||||||
|
Allow for a session based design - data needs to be collected per block
|
||||||
|
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
|
||||||
|
|
||||||
|
# Internal Notes
|
||||||
|
|
||||||
|
- Graphics Interaction Lab OptiTrack system is at 212.201.64.122
|
47
addons/vrpn/examples/optitrack.tscn
Normal file
47
addons/vrpn/examples/optitrack.tscn
Normal file
|
@ -0,0 +1,47 @@
|
||||||
|
[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="PackedScene" uid="uid://b426fy7d6jw2d" path="res://addons/vrpn/assets/axis.blend" id="3_73ywu"]
|
||||||
|
|
||||||
|
[sub_resource type="PlaneMesh" id="PlaneMesh_24d08"]
|
||||||
|
size = Vector2(6, 2)
|
||||||
|
|
||||||
|
[node name="Node3D" type="Node3D"]
|
||||||
|
|
||||||
|
[node name="Camera3D" type="Camera3D" parent="."]
|
||||||
|
transform = Transform3D(-1, 0, 8.74228e-08, 0, 1, 0, -8.74228e-08, 0, -1, 0, 0.355791, -1.59348)
|
||||||
|
current = true
|
||||||
|
|
||||||
|
[node name="DirectionalLight3D" type="DirectionalLight3D" parent="."]
|
||||||
|
transform = Transform3D(-0.878275, 0.266876, -0.396749, -7.71365e-11, 0.829749, 0.558137, 0.478155, 0.490197, -0.728748, 0, 0.631436, 0)
|
||||||
|
shadow_enabled = true
|
||||||
|
|
||||||
|
[node name="Root" type="Node3D" parent="."]
|
||||||
|
|
||||||
|
[node name="VRPN" type="Node3D" parent="Root" node_paths=PackedStringArray("tracker_receivers")]
|
||||||
|
script = ExtResource("1_jrm7s")
|
||||||
|
tracker_receivers = [NodePath("RB1"), NodePath("RB2")]
|
||||||
|
vrpn_server = "212.201.64.122"
|
||||||
|
|
||||||
|
[node name="RB1" type="Node3D" parent="Root/VRPN"]
|
||||||
|
script = ExtResource("2_fp2uy")
|
||||||
|
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"]
|
||||||
|
script = ExtResource("2_fp2uy")
|
||||||
|
tracker_name = "RB2"
|
||||||
|
|
||||||
|
[node name="axis" parent="Root/VRPN/RB2" instance=ExtResource("3_73ywu")]
|
||||||
|
transform = Transform3D(0.1, 0, 0, 0, 0.1, 0, 0, 0, 0.1, 0, 0, 0)
|
||||||
|
|
||||||
|
[node name="Floor" type="MeshInstance3D" parent="Root"]
|
||||||
|
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, -0.704947, 0)
|
||||||
|
mesh = SubResource("PlaneMesh_24d08")
|
||||||
|
|
||||||
|
[node name="Wall" type="MeshInstance3D" parent="Root"]
|
||||||
|
transform = Transform3D(-1, 8.74228e-08, -3.82137e-15, 0, -4.37114e-08, -1, -8.74228e-08, -1, 4.37114e-08, 0, 0.268571, 1.06259)
|
||||||
|
mesh = SubResource("PlaneMesh_24d08")
|
70
addons/vrpn/examples/spin_tracker.tscn
Normal file
70
addons/vrpn/examples/spin_tracker.tscn
Normal file
|
@ -0,0 +1,70 @@
|
||||||
|
[gd_scene load_steps=5 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="PackedScene" uid="uid://b426fy7d6jw2d" path="res://addons/vrpn/assets/axis.blend" id="3_170dk"]
|
||||||
|
|
||||||
|
[sub_resource type="PlaneMesh" id="PlaneMesh_24d08"]
|
||||||
|
size = Vector2(6, 2)
|
||||||
|
|
||||||
|
[node name="Node3D" type="Node3D"]
|
||||||
|
|
||||||
|
[node name="Camera3D" type="Camera3D" parent="."]
|
||||||
|
transform = Transform3D(1, 0, -1.74846e-07, 0, 1, 0, 1.74846e-07, 0, 1, 0, 0.933082, 2.80616)
|
||||||
|
|
||||||
|
[node name="DirectionalLight3D" type="DirectionalLight3D" parent="."]
|
||||||
|
transform = Transform3D(-0.999686, -0.0139775, 0.0207795, 0.0125216, 0.439603, 0.898105, -0.021688, 0.898083, -0.43929, 0, 1.59521, 0)
|
||||||
|
shadow_enabled = true
|
||||||
|
|
||||||
|
[node name="Root" type="Node3D" parent="."]
|
||||||
|
|
||||||
|
[node name="VRPN" type="Node3D" parent="Root" node_paths=PackedStringArray("tracker_receivers")]
|
||||||
|
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")
|
||||||
|
|
||||||
|
[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"]
|
||||||
|
script = ExtResource("2_170dk")
|
||||||
|
tracker_name = "Tracker1"
|
||||||
|
tracker_use_position = false
|
||||||
|
|
||||||
|
[node name="Axis" parent="Root/SpinTracker/Tracker1" instance=ExtResource("3_170dk")]
|
||||||
|
transform = Transform3D(0.1, 0, 0, 0, 0.1, 0, 0, 0, 0.1, 0, 0, 0)
|
||||||
|
|
||||||
|
[node name="Label-Y-Axis" type="Label3D" parent="Root/SpinTracker/Tracker1"]
|
||||||
|
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0.759683, 0)
|
||||||
|
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"]
|
||||||
|
script = ExtResource("2_170dk")
|
||||||
|
tracker_name = "Tracker2"
|
||||||
|
tracker_use_position = false
|
||||||
|
|
||||||
|
[node name="Axis" parent="Root/SpinTracker/Offset2/Tracker2" instance=ExtResource("3_170dk")]
|
||||||
|
transform = Transform3D(0.1, 0, 0, 0, 0.1, 0, 0, 0, 0.1, 0, 0, 0)
|
||||||
|
|
||||||
|
[node name="Label-Z-Axis" type="Label3D" parent="Root/SpinTracker/Offset2/Tracker2"]
|
||||||
|
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0.764205, 0)
|
||||||
|
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"]
|
||||||
|
script = ExtResource("2_170dk")
|
||||||
|
tracker_use_position = false
|
||||||
|
|
||||||
|
[node name="axis" parent="Root/SpinTracker/Offset0/Tracker0" instance=ExtResource("3_170dk")]
|
||||||
|
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"
|
Before Width: | Height: | Size: 994 B After Width: | Height: | Size: 994 B |
|
@ -3,15 +3,15 @@
|
||||||
importer="texture"
|
importer="texture"
|
||||||
type="CompressedTexture2D"
|
type="CompressedTexture2D"
|
||||||
uid="uid://cdprcmtx102rp"
|
uid="uid://cdprcmtx102rp"
|
||||||
path="res://.godot/imported/icon.svg-218a8f2b3041327d8a5756f3a245f83b.ctex"
|
path="res://.godot/imported/icon.svg-bbc889e9147eb1676401b9ec3d05066e.ctex"
|
||||||
metadata={
|
metadata={
|
||||||
"vram_texture": false
|
"vram_texture": false
|
||||||
}
|
}
|
||||||
|
|
||||||
[deps]
|
[deps]
|
||||||
|
|
||||||
source_file="res://icon.svg"
|
source_file="res://addons/vrpn/icons/icon.svg"
|
||||||
dest_files=["res://.godot/imported/icon.svg-218a8f2b3041327d8a5756f3a245f83b.ctex"]
|
dest_files=["res://.godot/imported/icon.svg-bbc889e9147eb1676401b9ec3d05066e.ctex"]
|
||||||
|
|
||||||
[params]
|
[params]
|
||||||
|
|
193
addons/vrpn/scripts/VRPN.gd
Normal file
193
addons/vrpn/scripts/VRPN.gd
Normal file
|
@ -0,0 +1,193 @@
|
||||||
|
extends Node
|
||||||
|
class_name VRPN
|
||||||
|
|
||||||
|
# tracking associated data
|
||||||
|
enum TrackingData { POS_QUAT, VELOCITY, ACCELERATION }
|
||||||
|
|
||||||
|
# magic cookie
|
||||||
|
const magic_cookie_start : String = "vrpn: ver."
|
||||||
|
# kinda redundant as we take the seq number as well
|
||||||
|
static var header_size : int = aligned_size(20)
|
||||||
|
|
||||||
|
|
||||||
|
var sensors : Dictionary[int,String] = {}
|
||||||
|
var messages : Dictionary[int,String] = {}
|
||||||
|
|
||||||
|
signal connected(s:StreamPeerTCP)
|
||||||
|
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
|
||||||
|
|
||||||
|
func _ready() -> void:
|
||||||
|
|
||||||
|
self.connect_to_host(vrpn_server,vrpn_port)
|
||||||
|
|
||||||
|
if not connected.has_connections():
|
||||||
|
connected.connect(self._on_connected)
|
||||||
|
if not data.has_connections():
|
||||||
|
data.connect(self._on_data)
|
||||||
|
if not disconnected.has_connections():
|
||||||
|
disconnected.connect(self._on_disconnected)
|
||||||
|
if not error.has_connections():
|
||||||
|
error.connect(self._on_error)
|
||||||
|
|
||||||
|
func _process(delta: float) -> void:
|
||||||
|
# store old state
|
||||||
|
var old_status = _stream.get_status()
|
||||||
|
# poll
|
||||||
|
_stream.poll()
|
||||||
|
# receive all errors
|
||||||
|
var new_status = _stream.get_status()
|
||||||
|
# process if something changed
|
||||||
|
if old_status != new_status:
|
||||||
|
match new_status:
|
||||||
|
_stream.STATUS_NONE:
|
||||||
|
emit_signal("disconnected")
|
||||||
|
_stream.STATUS_CONNECTING:
|
||||||
|
pass
|
||||||
|
_stream.STATUS_CONNECTED:
|
||||||
|
emit_signal("connected",_stream)
|
||||||
|
_stream.STATUS_ERROR:
|
||||||
|
emit_signal("error","error with socket stream from {0}:{1}".format([_stream.get_connected_host(),_stream.get_connected_port()]))
|
||||||
|
|
||||||
|
if new_status == _stream.STATUS_CONNECTED:
|
||||||
|
var available_bytes: int = _stream.get_available_bytes()
|
||||||
|
if available_bytes > 0:
|
||||||
|
var res = _stream.get_partial_data(available_bytes)
|
||||||
|
if res[0] != OK:
|
||||||
|
emit_signal("error","error receiving data {0}".format([res[0]]))
|
||||||
|
else:
|
||||||
|
emit_signal("data", res[1])
|
||||||
|
|
||||||
|
func connect_to_host(host: String, port: int) -> void:
|
||||||
|
if _stream.connect_to_host(host, port) != OK:
|
||||||
|
emit_signal("error","error connecting to host '{0}:{1}'".format([host,port]))
|
||||||
|
|
||||||
|
func send(data: PackedByteArray) -> bool:
|
||||||
|
if _stream.get_status() != _stream.STATUS_CONNECTED:
|
||||||
|
emit_signal("error","stream not connected!")
|
||||||
|
return false
|
||||||
|
var res: int = _stream.put_data(data)
|
||||||
|
if res != OK:
|
||||||
|
emit_signal("error","error writing to stream {0}!".format([res]))
|
||||||
|
return false
|
||||||
|
return true
|
||||||
|
|
||||||
|
|
||||||
|
func _on_data(data : Array):
|
||||||
|
var bytes = PackedByteArray(data)
|
||||||
|
var as_cookie = bytes.get_string_from_ascii()
|
||||||
|
|
||||||
|
# Cookie Hack!
|
||||||
|
if as_cookie.begins_with(VRPN.magic_cookie_start): #
|
||||||
|
# kaboom we just send back the same cookie :)
|
||||||
|
self.send(bytes)
|
||||||
|
else:
|
||||||
|
VRPN.marshall_block(bytes,self)
|
||||||
|
|
||||||
|
|
||||||
|
func _on_connected(s : StreamPeerTCP):
|
||||||
|
print("Connected to",s.get_connected_host()) # Replace with function body.
|
||||||
|
|
||||||
|
func _on_disconnected():
|
||||||
|
push_warning("Disconnected") # Replace with function body.
|
||||||
|
|
||||||
|
func _on_error(msg:String):
|
||||||
|
push_warning(msg) # Replace with function body.
|
||||||
|
|
||||||
|
|
||||||
|
static func marshall_block(data : PackedByteArray,session : VRPN) -> void:
|
||||||
|
|
||||||
|
# need to fix that
|
||||||
|
var block_offset : int = 0
|
||||||
|
|
||||||
|
while data.size() > block_offset:
|
||||||
|
# reader for stream
|
||||||
|
var header := StreamPeerBuffer.new()
|
||||||
|
# get block addresses
|
||||||
|
header.data_array = data.slice(block_offset,block_offset+header_size)
|
||||||
|
# make sure we read as big endian
|
||||||
|
header.big_endian = true
|
||||||
|
|
||||||
|
# read header
|
||||||
|
var length := header.get_32() as int # length of message
|
||||||
|
var time_sec := header.get_32() as int # datetime sec
|
||||||
|
var time_msec := header.get_32() as int # datetime micro sec
|
||||||
|
var sender_id := header.get_32() as int # sender id (tracker or interfaces)
|
||||||
|
var message_type := header.get_32() as int # type of message (payload)
|
||||||
|
var sequence_num := header.get_32() as int # inofficial sequence number (padding)
|
||||||
|
|
||||||
|
# for debugging
|
||||||
|
#print("length '%d'" % length)
|
||||||
|
#print("time_sec '%d'" % time_sec)
|
||||||
|
#print("time_msec '%d'" % time_msec)
|
||||||
|
#print("sender_id '%d'" % sender_id)
|
||||||
|
#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)
|
||||||
|
|
||||||
|
# next datablock
|
||||||
|
block_offset += aligned_size(length)
|
||||||
|
|
||||||
|
static func decode_string(stream : StreamPeerBuffer) -> String:
|
||||||
|
var len = stream.get_32()
|
||||||
|
return stream.get_string(len)
|
||||||
|
|
||||||
|
|
||||||
|
static func marshall_body(data : PackedByteArray,message_type : int, sender_id: int, session : VRPN):
|
||||||
|
var body := StreamPeerBuffer.new()
|
||||||
|
body.data_array = data
|
||||||
|
body.big_endian = true
|
||||||
|
|
||||||
|
# only take message_type directly for negative (-1,-2)
|
||||||
|
# messages that provide dynamic descriptors
|
||||||
|
if message_type < 0:
|
||||||
|
# message and sender descriptions
|
||||||
|
match message_type:
|
||||||
|
-1: # sensor names
|
||||||
|
var name = decode_string(body)
|
||||||
|
print("sensor name is '%s' with '%d" % [name,sender_id])
|
||||||
|
session.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
|
||||||
|
return
|
||||||
|
|
||||||
|
# now we use the string identifiers
|
||||||
|
# because they are supposedly dynamically assigned
|
||||||
|
match session.messages[message_type]:
|
||||||
|
'vrpn_Tracker Pos_Quat': # quat pos
|
||||||
|
# get id
|
||||||
|
var sensor_id = body.get_32()
|
||||||
|
var padding = body.get_32() # padding
|
||||||
|
# position
|
||||||
|
var pos = 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 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],
|
||||||
|
"sensor" : sensor_id,
|
||||||
|
"position" : pos,
|
||||||
|
"rotation" : quat
|
||||||
|
})
|
||||||
|
'vrpn_Tracker Velocity':
|
||||||
|
pass
|
||||||
|
_:
|
||||||
|
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)
|
18
addons/vrpn/scripts/VRPN_Receiver.gd
Normal file
18
addons/vrpn/scripts/VRPN_Receiver.gd
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
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)
|
1
addons/vrpn/scripts/VRPN_Receiver.gd.uid
Normal file
1
addons/vrpn/scripts/VRPN_Receiver.gd.uid
Normal file
|
@ -0,0 +1 @@
|
||||||
|
uid://dpj1wrvfsiq4v
|
|
@ -40,7 +40,9 @@
|
||||||
# float z_of_axis_to_spin_around
|
# float z_of_axis_to_spin_around
|
||||||
# float rotation_rate_around_axis_in_Hz
|
# float rotation_rate_around_axis_in_Hz
|
||||||
|
|
||||||
vrpn_Tracker_Spin Tracker0 1 200.0 0.0 1.0 0.0 0.1
|
vrpn_Tracker_Spin Tracker0 1 200.0 1.0 0.0 0.0 0.1
|
||||||
|
vrpn_Tracker_Spin Tracker1 1 200.0 0.0 1.0 0.0 0.2
|
||||||
|
vrpn_Tracker_Spin Tracker2 1 200.0 0.0 0.0 1.0 0.3
|
||||||
|
|
||||||
################################################################################
|
################################################################################
|
||||||
# WintrackerIII from VR SPace
|
# WintrackerIII from VR SPace
|
|
@ -13,4 +13,4 @@ config_version=5
|
||||||
config/name="uvrpn"
|
config/name="uvrpn"
|
||||||
run/main_scene="uid://bj5ykdjle10tt"
|
run/main_scene="uid://bj5ykdjle10tt"
|
||||||
config/features=PackedStringArray("4.4", "Forward Plus")
|
config/features=PackedStringArray("4.4", "Forward Plus")
|
||||||
config/icon="res://icon.svg"
|
config/icon="uid://cdprcmtx102rp"
|
||||||
|
|
50
uvrpn.gd
50
uvrpn.gd
|
@ -1,50 +0,0 @@
|
||||||
extends Node
|
|
||||||
|
|
||||||
@export var url : String = "localhost"
|
|
||||||
@export var port : int = 3883
|
|
||||||
|
|
||||||
var vrpn_cookie : String = "vrpn: ver. 07.35"
|
|
||||||
|
|
||||||
#var server : UDPServer = null
|
|
||||||
@onready var socket : StreamPeerTCP = StreamPeerTCP.new()
|
|
||||||
|
|
||||||
func _ready() -> void:
|
|
||||||
#if StreamPeerTCP.STATUS_NONE == socket.get_status():
|
|
||||||
if socket.connect_to_host(url,port) == OK:
|
|
||||||
print("Socket connected ...")
|
|
||||||
socket.set_no_delay(true)
|
|
||||||
send_data(socket,vrpn_cookie.to_utf8_buffer())
|
|
||||||
#server = UDPServer.new()
|
|
||||||
#server.listen(3883)
|
|
||||||
else:
|
|
||||||
print("Error connecting to server")
|
|
||||||
#else:
|
|
||||||
#print("Stream not ready")
|
|
||||||
|
|
||||||
|
|
||||||
func _process(delta: float) -> void:
|
|
||||||
if socket:
|
|
||||||
socket.poll()
|
|
||||||
##return
|
|
||||||
##if server:
|
|
||||||
##server.poll()
|
|
||||||
##if server.is_connection_available():
|
|
||||||
##var peer = server.take_connection()
|
|
||||||
##var packet = peer.get_packet()
|
|
||||||
##print("Accepted peer: %s:%s" % [peer.get_packet_ip(), peer.get_packet_port()])
|
|
||||||
##print("Received data: %s" % [packet.get_string_from_utf8()])
|
|
||||||
### Reply so it knows we received the message.
|
|
||||||
##peer.put_packet(packet)
|
|
||||||
|
|
||||||
|
|
||||||
static func send_data(socket : StreamPeerTCP, data: PackedByteArray) -> bool:
|
|
||||||
print(socket.get_status())
|
|
||||||
if socket.get_status() == StreamPeerTCP.STATUS_CONNECTED:
|
|
||||||
if socket.put_data(data) == OK:
|
|
||||||
return true
|
|
||||||
else:
|
|
||||||
print("Error writing data ...")
|
|
||||||
return false
|
|
||||||
else:
|
|
||||||
print("Error connecting")
|
|
||||||
return false
|
|
|
@ -1 +0,0 @@
|
||||||
uid://ca5psnjx63ua8
|
|
Loading…
Add table
Add a link
Reference in a new issue