diff --git a/README.md b/README.md index b5ddebe..8328dfa 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/addons/vrpn/docs/Examples.md b/addons/vrpn/docs/Examples.md new file mode 100644 index 0000000..e158fa2 --- /dev/null +++ b/addons/vrpn/docs/Examples.md @@ -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 +``` diff --git a/addons/vrpn/docs/Notes.md b/addons/vrpn/docs/Notes.md index 9854ae2..8f3ae7f 100644 --- a/addons/vrpn/docs/Notes.md +++ b/addons/vrpn/docs/Notes.md @@ -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 diff --git a/addons/vrpn/docs/img/.gdignore b/addons/vrpn/docs/img/.gdignore new file mode 100644 index 0000000..e69de29 diff --git a/addons/vrpn/docs/img/gdvrpn-optitrack.jpg b/addons/vrpn/docs/img/gdvrpn-optitrack.jpg new file mode 100644 index 0000000..4609741 Binary files /dev/null and b/addons/vrpn/docs/img/gdvrpn-optitrack.jpg differ diff --git a/addons/vrpn/examples/optitrack.tscn b/addons/vrpn/examples/optitrack.tscn index 819a329..3c4e20b 100644 --- a/addons/vrpn/examples/optitrack.tscn +++ b/addons/vrpn/examples/optitrack.tscn @@ -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")] diff --git a/addons/vrpn/examples/spin_tracker.tscn b/addons/vrpn/examples/spin_tracker.tscn index cb3e429..4a973d2 100644 --- a/addons/vrpn/examples/spin_tracker.tscn +++ b/addons/vrpn/examples/spin_tracker.tscn @@ -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"] diff --git a/addons/vrpn/icons/icon.png b/addons/vrpn/icons/icon.png new file mode 100644 index 0000000..4099d5c Binary files /dev/null and b/addons/vrpn/icons/icon.png differ diff --git a/addons/vrpn/icons/icon.png.import b/addons/vrpn/icons/icon.png.import new file mode 100644 index 0000000..bbdf861 --- /dev/null +++ b/addons/vrpn/icons/icon.png.import @@ -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 diff --git a/addons/vrpn/icons/icon.svg b/addons/vrpn/icons/icon.svg index 9d8b7fa..8ac5fb2 100644 --- a/addons/vrpn/icons/icon.svg +++ b/addons/vrpn/icons/icon.svg @@ -1 +1,89 @@ - \ No newline at end of file + + + + + + + + + + + + + + + + diff --git a/addons/vrpn/scripts/VRPN_Button.gd b/addons/vrpn/scripts/VRPN_Button.gd new file mode 100644 index 0000000..750b8b1 --- /dev/null +++ b/addons/vrpn/scripts/VRPN_Button.gd @@ -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) diff --git a/addons/vrpn/scripts/VRPN_Button.gd.uid b/addons/vrpn/scripts/VRPN_Button.gd.uid new file mode 100644 index 0000000..862cc52 --- /dev/null +++ b/addons/vrpn/scripts/VRPN_Button.gd.uid @@ -0,0 +1 @@ +uid://bmlyip5xa5df4 diff --git a/addons/vrpn/scripts/VRPN.gd b/addons/vrpn/scripts/VRPN_Client.gd similarity index 67% rename from addons/vrpn/scripts/VRPN.gd rename to addons/vrpn/scripts/VRPN_Client.gd index eecd554..2902d31 100644 --- a/addons/vrpn/scripts/VRPN.gd +++ b/addons/vrpn/scripts/VRPN_Client.gd @@ -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) diff --git a/addons/vrpn/scripts/VRPN.gd.uid b/addons/vrpn/scripts/VRPN_Client.gd.uid similarity index 100% rename from addons/vrpn/scripts/VRPN.gd.uid rename to addons/vrpn/scripts/VRPN_Client.gd.uid diff --git a/addons/vrpn/scripts/VRPN_Receiver.gd b/addons/vrpn/scripts/VRPN_Receiver.gd deleted file mode 100644 index a9654e3..0000000 --- a/addons/vrpn/scripts/VRPN_Receiver.gd +++ /dev/null @@ -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) diff --git a/addons/vrpn/scripts/VRPN_Tracker.gd b/addons/vrpn/scripts/VRPN_Tracker.gd new file mode 100644 index 0000000..f9788c4 --- /dev/null +++ b/addons/vrpn/scripts/VRPN_Tracker.gd @@ -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. diff --git a/addons/vrpn/scripts/VRPN_Receiver.gd.uid b/addons/vrpn/scripts/VRPN_Tracker.gd.uid similarity index 100% rename from addons/vrpn/scripts/VRPN_Receiver.gd.uid rename to addons/vrpn/scripts/VRPN_Tracker.gd.uid diff --git a/project.godot b/project.godot index 19b923a..21af7b7 100644 --- a/project.godot +++ b/project.godot @@ -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