From 2cc6c4bd1cdabb4efd15babf9c267bae9a63eccb Mon Sep 17 00:00:00 2001 From: Ivan-267 <61947090+Ivan-267@users.noreply.github.com> Date: Sun, 7 Apr 2024 18:57:06 +0200 Subject: [PATCH] Virtual Camera update: Rllib and onnx support, added onnx --- examples/VirtualCamera/Env.tscn | 1 + examples/VirtualCamera/Player.gd | 3 +- examples/VirtualCamera/Player.tscn | 9 +- examples/VirtualCamera/VirtualCamera.csproj | 11 + examples/VirtualCamera/VirtualCamera.onnx | Bin 0 -> 63249 bytes examples/VirtualCamera/VirtualCamera.sln | 19 + .../controller/ai_controller_2d.gd | 97 ++-- .../controller/ai_controller_3d.gd | 96 +++- .../onnx/csharp/ONNXInference.cs | 14 +- .../onnx/wrapper/ONNX_wrapper.gd | 35 +- .../sensors/sensors_2d/GridSensor2D.gd | 137 +++-- .../sensors/sensors_2d/ISensor2D.gd | 13 +- .../sensors/sensors_2d/RaycastSensor2D.gd | 80 +-- .../sensors/sensors_3d/GridSensor3D.gd | 149 +++--- .../sensors/sensors_3d/ISensor3D.gd | 13 +- .../sensors/sensors_3d/RGBCameraSensor3D.gd | 62 ++- .../sensors/sensors_3d/RGBCameraSensor3D.tscn | 30 +- .../sensors/sensors_3d/RaycastSensor3D.gd | 97 ++-- .../addons/godot_rl_agents/sync.gd | 503 +++++++++++++----- examples/VirtualCamera/project.godot | 12 +- 20 files changed, 942 insertions(+), 439 deletions(-) create mode 100644 examples/VirtualCamera/VirtualCamera.csproj create mode 100644 examples/VirtualCamera/VirtualCamera.onnx create mode 100644 examples/VirtualCamera/VirtualCamera.sln diff --git a/examples/VirtualCamera/Env.tscn b/examples/VirtualCamera/Env.tscn index 269ee0d..bb161a0 100644 --- a/examples/VirtualCamera/Env.tscn +++ b/examples/VirtualCamera/Env.tscn @@ -56,6 +56,7 @@ transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 63, 0, -50) [node name="Sync" type="Node" parent="."] process_priority = -1 script = ExtResource("2") +onnx_model_path = "VirtualCamera.onnx" [node name="Camera" type="Camera3D" parent="."] transform = Transform3D(1, 0, 0, 0, 0.0220418, 0.999757, 0, -0.999757, 0.0220418, 25.3538, 75.4275, -10.0795) diff --git a/examples/VirtualCamera/Player.gd b/examples/VirtualCamera/Player.gd index 02c4586..144dd44 100644 --- a/examples/VirtualCamera/Player.gd +++ b/examples/VirtualCamera/Player.gd @@ -113,7 +113,8 @@ func reset(): func update_reward(): - ai_controller.reward -= 0.01 # step penalty + #ai_controller.reward -= 0.01 # step penalty + pass func calculate_translation(other_pad_translation: Vector3) -> Vector3: diff --git a/examples/VirtualCamera/Player.tscn b/examples/VirtualCamera/Player.tscn index e49a276..fe82295 100644 --- a/examples/VirtualCamera/Player.tscn +++ b/examples/VirtualCamera/Player.tscn @@ -2,8 +2,8 @@ [ext_resource type="Script" path="res://Player.gd" id="1"] [ext_resource type="PackedScene" uid="uid://b4hphc8dab5h" path="res://Robot.tscn" id="2"] -[ext_resource type="PackedScene" uid="uid://b30vsuwotx0u2" path="res://VirtualCamera.tscn" id="3_tv0v5"] [ext_resource type="Script" path="res://AIController3D.gd" id="4_rq7t7"] +[ext_resource type="PackedScene" uid="uid://baaywi3arsl2m" path="res://addons/godot_rl_agents/sensors/sensors_3d/RGBCameraSensor3D.tscn" id="4_ybcln"] [sub_resource type="CapsuleShape3D" id="1"] radius = 1.6 @@ -27,8 +27,11 @@ mesh = SubResource("2") [node name="Robot" parent="." instance=ExtResource("2")] transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, -1.43952, 0.0576344) -[node name="RGBCameraSensor3D" parent="." instance=ExtResource("3_tv0v5")] - [node name="AIController3D" type="Node3D" parent="."] script = ExtResource("4_rq7t7") reset_after = 10000 + +[node name="RGBCameraSensor3D" parent="." instance=ExtResource("4_ybcln")] +training_mode = true +render_image_resolution = Vector2(10, 10) +displayed_image_scale_factor = Vector2(20, 20) diff --git a/examples/VirtualCamera/VirtualCamera.csproj b/examples/VirtualCamera/VirtualCamera.csproj new file mode 100644 index 0000000..9e0f6e5 --- /dev/null +++ b/examples/VirtualCamera/VirtualCamera.csproj @@ -0,0 +1,11 @@ + + + net6.0 + net7.0 + net8.0 + true + + + + + \ No newline at end of file diff --git a/examples/VirtualCamera/VirtualCamera.onnx b/examples/VirtualCamera/VirtualCamera.onnx new file mode 100644 index 0000000000000000000000000000000000000000..6f20344aad0d5971143ecee3dfae340f013edc73 GIT binary patch literal 63249 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 Xw$N`~NMulC?EfD9@4IZp Dictionary: - assert(false, "the get_obs method is not implemented when extending from ai_controller") - return {"obs":[]} + assert(false, "the get_obs method is not implemented when extending from ai_controller") + return {"obs": []} -func get_reward() -> float: - assert(false, "the get_reward method is not implemented when extending from ai_controller") + +func get_reward() -> float: + assert(false, "the get_reward method is not implemented when extending from ai_controller") return 0.0 - + + func get_action_space() -> Dictionary: - assert(false, "the get get_action_space method is not implemented when extending from ai_controller") + assert( + false, + "the get get_action_space method is not implemented when extending from ai_controller" + ) return { - "example_actions_continous" : { - "size": 2, - "action_type": "continuous" - }, - "example_actions_discrete" : { - "size": 2, - "action_type": "discrete" - }, - } - -func set_action(action) -> void: - assert(false, "the get set_action method is not implemented when extending from ai_controller") + "example_actions_continous": {"size": 2, "action_type": "continuous"}, + "example_actions_discrete": {"size": 2, "action_type": "discrete"}, + } + + +func set_action(action) -> void: + assert(false, "the set_action method is not implemented when extending from ai_controller") + + +#-----------------------------------------------------------------------------# + + +#-- Methods that sometimes need implementing using the "extend script" option in Godot --# +# Only needed if you are recording expert demos with this AIController +func get_action() -> Array: + assert(false, "the get_action method is not implemented in extended AIController but demo_recorder is used") + return [] + # -----------------------------------------------------------------------------# - + func _physics_process(delta): n_steps += 1 if n_steps > reset_after: needs_reset = true - + + func get_obs_space(): # may need overriding if the obs space is complex var obs = get_obs() return { - "obs": { - "size": [len(obs["obs"])], - "space": "box" - }, + "obs": {"size": [len(obs["obs"])], "space": "box"}, } + func reset(): n_steps = 0 needs_reset = false + func reset_if_done(): if done: reset() - + + func set_heuristic(h): # sets the heuristic from "human" or "model" nothing to change here heuristic = h + func get_done(): return done - + + func set_done_false(): done = false + func zero_reward(): reward = 0.0 - - diff --git a/examples/VirtualCamera/addons/godot_rl_agents/controller/ai_controller_3d.gd b/examples/VirtualCamera/addons/godot_rl_agents/controller/ai_controller_3d.gd index d256b2a..c77d9e0 100644 --- a/examples/VirtualCamera/addons/godot_rl_agents/controller/ai_controller_3d.gd +++ b/examples/VirtualCamera/addons/godot_rl_agents/controller/ai_controller_3d.gd @@ -1,8 +1,29 @@ extends Node3D class_name AIController3D +enum ControlModes { INHERIT_FROM_SYNC, HUMAN, TRAINING, ONNX_INFERENCE, RECORD_EXPERT_DEMOS } +@export var control_mode: ControlModes = ControlModes.INHERIT_FROM_SYNC +@export var onnx_model_path := "" @export var reset_after := 1000 +@export_group("Record expert demos mode options") +## Path where the demos will be saved. The file can later be used for imitation learning. +@export var expert_demo_save_path: String +## The action that erases the last recorded episode from the currently recorded data. +@export var remove_last_episode_key: InputEvent +## Action will be repeated for n frames. Will introduce control lag if larger than 1. +## Can be used to ensure that action_repeat on inference and training matches +## the recorded demonstrations. +@export var action_repeat: int = 1 + +@export_group("Multi-policy mode options") +## Allows you to set certain agents to use different policies. +## Changing has no effect with default SB3 training. Works with Rllib example. +## Tutorial: https://github.com/edbeeching/godot_rl_agents/blob/main/docs/TRAINING_MULTIPLE_POLICIES.md +@export var policy_name: String = "shared_policy" + +var onnx_model: ONNXModel + var heuristic := "human" var done := false var reward := 0.0 @@ -11,70 +32,89 @@ var needs_reset := false var _player: Node3D + func _ready(): add_to_group("AGENT") - + + func init(player: Node3D): _player = player - + + #-- Methods that need implementing using the "extend script" option in Godot --# func get_obs() -> Dictionary: - assert(false, "the get_obs method is not implemented when extending from ai_controller") - return {"obs":[]} + assert(false, "the get_obs method is not implemented when extending from ai_controller") + return {"obs": []} -func get_reward() -> float: - assert(false, "the get_reward method is not implemented when extending from ai_controller") + +func get_reward() -> float: + assert(false, "the get_reward method is not implemented when extending from ai_controller") return 0.0 - + + func get_action_space() -> Dictionary: - assert(false, "the get get_action_space method is not implemented when extending from ai_controller") + assert( + false, + "the get_action_space method is not implemented when extending from ai_controller" + ) return { - "example_actions_continous" : { - "size": 2, - "action_type": "continuous" - }, - "example_actions_discrete" : { - "size": 2, - "action_type": "discrete" - }, - } - -func set_action(action) -> void: - assert(false, "the get set_action method is not implemented when extending from ai_controller") + "example_actions_continous": {"size": 2, "action_type": "continuous"}, + "example_actions_discrete": {"size": 2, "action_type": "discrete"}, + } + + +func set_action(action) -> void: + assert(false, "the set_action method is not implemented when extending from ai_controller") + + +#-----------------------------------------------------------------------------# + + +#-- Methods that sometimes need implementing using the "extend script" option in Godot --# +# Only needed if you are recording expert demos with this AIController +func get_action() -> Array: + assert(false, "the get_action method is not implemented in extended AIController but demo_recorder is used") + return [] + # -----------------------------------------------------------------------------# - + + func _physics_process(delta): n_steps += 1 if n_steps > reset_after: needs_reset = true - + + func get_obs_space(): # may need overriding if the obs space is complex var obs = get_obs() return { - "obs": { - "size": [len(obs["obs"])], - "space": "box" - }, + "obs": {"size": [len(obs["obs"])], "space": "box"}, } + func reset(): n_steps = 0 needs_reset = false + func reset_if_done(): if done: reset() - + + func set_heuristic(h): # sets the heuristic from "human" or "model" nothing to change here heuristic = h + func get_done(): return done - + + func set_done_false(): done = false + func zero_reward(): reward = 0.0 diff --git a/examples/VirtualCamera/addons/godot_rl_agents/onnx/csharp/ONNXInference.cs b/examples/VirtualCamera/addons/godot_rl_agents/onnx/csharp/ONNXInference.cs index 0741f0f..6dcfa18 100644 --- a/examples/VirtualCamera/addons/godot_rl_agents/onnx/csharp/ONNXInference.cs +++ b/examples/VirtualCamera/addons/godot_rl_agents/onnx/csharp/ONNXInference.cs @@ -19,16 +19,22 @@ public partial class ONNXInference : GodotObject private SessionOptions SessionOpt; - //init function - /// - public void Initialize(string Path, int BatchSize) + /// + /// init function + /// + /// + /// + /// Returns the output size of the model + public int Initialize(string Path, int BatchSize) { modelPath = Path; batchSize = BatchSize; SessionOpt = SessionConfigurator.MakeConfiguredSessionOptions(); session = LoadModel(modelPath); + return session.OutputMetadata["output"].Dimensions[1]; + } + - } /// public Godot.Collections.Dictionary> RunInference(Godot.Collections.Array obs, int state_ins) { diff --git a/examples/VirtualCamera/addons/godot_rl_agents/onnx/wrapper/ONNX_wrapper.gd b/examples/VirtualCamera/addons/godot_rl_agents/onnx/wrapper/ONNX_wrapper.gd index c7b14b3..e27f2c3 100644 --- a/examples/VirtualCamera/addons/godot_rl_agents/onnx/wrapper/ONNX_wrapper.gd +++ b/examples/VirtualCamera/addons/godot_rl_agents/onnx/wrapper/ONNX_wrapper.gd @@ -4,21 +4,48 @@ var inferencer_script = load("res://addons/godot_rl_agents/onnx/csharp/ONNXInfer var inferencer = null +## How many action values the model outputs +var action_output_size: int + +## Used to differentiate models +## that only output continuous action mean (e.g. sb3, cleanrl export) +## versus models that output mean and logstd (e.g. rllib export) +var action_means_only: bool + +## Whether action_means_value has been set already for this model +var action_means_only_set: bool + # Must provide the path to the model and the batch size func _init(model_path, batch_size): inferencer = inferencer_script.new() - inferencer.Initialize(model_path, batch_size) + action_output_size = inferencer.Initialize(model_path, batch_size) -# This function is the one that will be called from the game, +# This function is the one that will be called from the game, # requires the observation as an array and the state_ins as an int -# returns an Array containing the action the model takes. -func run_inference(obs : Array, state_ins : int) -> Dictionary: +# returns an Array containing the action the model takes. +func run_inference(obs: Array, state_ins: int) -> Dictionary: if inferencer == null: printerr("Inferencer not initialized") return {} return inferencer.RunInference(obs, state_ins) + func _notification(what): if what == NOTIFICATION_PREDELETE: inferencer.FreeDisposables() inferencer.free() + +# Check whether agent uses a continuous actions model with only action means or not +func set_action_means_only(agent_action_space): + action_means_only_set = true + var continuous_only: bool = true + var continuous_actions: int + for action in agent_action_space: + if not agent_action_space[action]["action_type"] == "continuous": + continuous_only = false + break + else: + continuous_actions += agent_action_space[action]["size"] + if continuous_only: + if continuous_actions == action_output_size: + action_means_only = true diff --git a/examples/VirtualCamera/addons/godot_rl_agents/sensors/sensors_2d/GridSensor2D.gd b/examples/VirtualCamera/addons/godot_rl_agents/sensors/sensors_2d/GridSensor2D.gd index 12f2957..da170ba 100644 --- a/examples/VirtualCamera/addons/godot_rl_agents/sensors/sensors_2d/GridSensor2D.gd +++ b/examples/VirtualCamera/addons/godot_rl_agents/sensors/sensors_2d/GridSensor2D.gd @@ -3,49 +3,57 @@ extends ISensor2D class_name GridSensor2D @export var debug_view := false: - get: return debug_view + get: + return debug_view set(value): debug_view = value _update() - + @export_flags_2d_physics var detection_mask := 0: - get: return detection_mask + get: + return detection_mask set(value): detection_mask = value _update() @export var collide_with_areas := false: - get: return collide_with_areas + get: + return collide_with_areas set(value): collide_with_areas = value _update() @export var collide_with_bodies := true: - get: return collide_with_bodies + get: + return collide_with_bodies set(value): collide_with_bodies = value _update() @export_range(1, 200, 0.1) var cell_width := 20.0: - get: return cell_width + get: + return cell_width set(value): cell_width = value _update() @export_range(1, 200, 0.1) var cell_height := 20.0: - get: return cell_height + get: + return cell_height set(value): cell_height = value - _update() + _update() @export_range(1, 21, 2, "or_greater") var grid_size_x := 3: - get: return grid_size_x + get: + return grid_size_x set(value): grid_size_x = value _update() @export_range(1, 21, 2, "or_greater") var grid_size_y := 3: - get: return grid_size_y + get: + return grid_size_y set(value): grid_size_y = value _update() @@ -58,158 +66,169 @@ var _n_layers_per_cell: int var _highlighted_cell_color: Color var _standard_cell_color: Color + func get_observation(): return _obs_buffer - + + func _update(): if Engine.is_editor_hint(): if is_node_ready(): - _spawn_nodes() + _spawn_nodes() + func _ready() -> void: _set_colors() - - if Engine.is_editor_hint(): + + if Engine.is_editor_hint(): if get_child_count() == 0: _spawn_nodes() else: _spawn_nodes() - - + + func _set_colors() -> void: - _standard_cell_color = Color(100.0/255.0, 100.0/255.0, 100.0/255.0, 100.0/255.0) - _highlighted_cell_color = Color(255.0/255.0, 100.0/255.0, 100.0/255.0, 100.0/255.0) + _standard_cell_color = Color(100.0 / 255.0, 100.0 / 255.0, 100.0 / 255.0, 100.0 / 255.0) + _highlighted_cell_color = Color(255.0 / 255.0, 100.0 / 255.0, 100.0 / 255.0, 100.0 / 255.0) + func _get_collision_mapping() -> Dictionary: # defines which layer is mapped to which cell obs index var total_bits = 0 - var collision_mapping = {} + var collision_mapping = {} for i in 32: - var bit_mask = 2**i + var bit_mask = 2 ** i if (detection_mask & bit_mask) > 0: collision_mapping[i] = total_bits total_bits += 1 - + return collision_mapping + func _spawn_nodes(): for cell in get_children(): - cell.name = "_%s" % cell.name # Otherwise naming below will fail + cell.name = "_%s" % cell.name # Otherwise naming below will fail cell.queue_free() - + _collision_mapping = _get_collision_mapping() #prints("collision_mapping", _collision_mapping, len(_collision_mapping)) # allocate memory for the observations _n_layers_per_cell = len(_collision_mapping) _obs_buffer = PackedFloat64Array() - _obs_buffer.resize(grid_size_x*grid_size_y*_n_layers_per_cell) + _obs_buffer.resize(grid_size_x * grid_size_y * _n_layers_per_cell) _obs_buffer.fill(0) #prints(len(_obs_buffer), _obs_buffer ) - + _rectangle_shape = RectangleShape2D.new() _rectangle_shape.set_size(Vector2(cell_width, cell_height)) - + var shift := Vector2( - -(grid_size_x/2)*cell_width, - -(grid_size_y/2)*cell_height, + -(grid_size_x / 2) * cell_width, + -(grid_size_y / 2) * cell_height, ) - + for i in grid_size_x: for j in grid_size_y: - var cell_position = Vector2(i*cell_width, j*cell_height) + shift + var cell_position = Vector2(i * cell_width, j * cell_height) + shift _create_cell(i, j, cell_position) - -func _create_cell(i:int, j:int, position: Vector2): - var cell : = Area2D.new() + +func _create_cell(i: int, j: int, position: Vector2): + var cell := Area2D.new() cell.position = position - cell.name = "GridCell %s %s" %[i, j] + cell.name = "GridCell %s %s" % [i, j] cell.modulate = _standard_cell_color - + if collide_with_areas: cell.area_entered.connect(_on_cell_area_entered.bind(i, j)) cell.area_exited.connect(_on_cell_area_exited.bind(i, j)) - + if collide_with_bodies: cell.body_entered.connect(_on_cell_body_entered.bind(i, j)) cell.body_exited.connect(_on_cell_body_exited.bind(i, j)) - + cell.collision_layer = 0 cell.collision_mask = detection_mask cell.monitorable = true add_child(cell) cell.set_owner(get_tree().edited_scene_root) - var col_shape : = CollisionShape2D.new() + var col_shape := CollisionShape2D.new() col_shape.shape = _rectangle_shape col_shape.name = "CollisionShape2D" cell.add_child(col_shape) col_shape.set_owner(get_tree().edited_scene_root) - + if debug_view: var quad = MeshInstance2D.new() quad.name = "MeshInstance2D" var quad_mesh = QuadMesh.new() - + quad_mesh.set_size(Vector2(cell_width, cell_height)) - + quad.mesh = quad_mesh cell.add_child(quad) quad.set_owner(get_tree().edited_scene_root) -func _update_obs(cell_i:int, cell_j:int, collision_layer:int, entered: bool): + +func _update_obs(cell_i: int, cell_j: int, collision_layer: int, entered: bool): for key in _collision_mapping: - var bit_mask = 2**key + var bit_mask = 2 ** key if (collision_layer & bit_mask) > 0: var collison_map_index = _collision_mapping[key] - + var obs_index = ( - (cell_i * grid_size_x * _n_layers_per_cell) + - (cell_j * _n_layers_per_cell) + - collison_map_index - ) + (cell_i * grid_size_x * _n_layers_per_cell) + + (cell_j * _n_layers_per_cell) + + collison_map_index + ) #prints(obs_index, cell_i, cell_j) if entered: _obs_buffer[obs_index] += 1 else: _obs_buffer[obs_index] -= 1 -func _toggle_cell(cell_i:int, cell_j:int): - var cell = get_node_or_null("GridCell %s %s" %[cell_i, cell_j]) - + +func _toggle_cell(cell_i: int, cell_j: int): + var cell = get_node_or_null("GridCell %s %s" % [cell_i, cell_j]) + if cell == null: print("cell not found, returning") - + var n_hits = 0 var start_index = (cell_i * grid_size_x * _n_layers_per_cell) + (cell_j * _n_layers_per_cell) for i in _n_layers_per_cell: - n_hits += _obs_buffer[start_index+i] - + n_hits += _obs_buffer[start_index + i] + if n_hits > 0: cell.modulate = _highlighted_cell_color else: cell.modulate = _standard_cell_color - -func _on_cell_area_entered(area:Area2D, cell_i:int, cell_j:int): + + +func _on_cell_area_entered(area: Area2D, cell_i: int, cell_j: int): #prints("_on_cell_area_entered", cell_i, cell_j) _update_obs(cell_i, cell_j, area.collision_layer, true) if debug_view: _toggle_cell(cell_i, cell_j) #print(_obs_buffer) -func _on_cell_area_exited(area:Area2D, cell_i:int, cell_j:int): + +func _on_cell_area_exited(area: Area2D, cell_i: int, cell_j: int): #prints("_on_cell_area_exited", cell_i, cell_j) _update_obs(cell_i, cell_j, area.collision_layer, false) if debug_view: _toggle_cell(cell_i, cell_j) -func _on_cell_body_entered(body: Node2D, cell_i:int, cell_j:int): + +func _on_cell_body_entered(body: Node2D, cell_i: int, cell_j: int): #prints("_on_cell_body_entered", cell_i, cell_j) _update_obs(cell_i, cell_j, body.collision_layer, true) if debug_view: _toggle_cell(cell_i, cell_j) -func _on_cell_body_exited(body: Node2D, cell_i:int, cell_j:int): + +func _on_cell_body_exited(body: Node2D, cell_i: int, cell_j: int): #prints("_on_cell_body_exited", cell_i, cell_j) _update_obs(cell_i, cell_j, body.collision_layer, false) if debug_view: diff --git a/examples/VirtualCamera/addons/godot_rl_agents/sensors/sensors_2d/ISensor2D.gd b/examples/VirtualCamera/addons/godot_rl_agents/sensors/sensors_2d/ISensor2D.gd index ec20f08..67669a1 100644 --- a/examples/VirtualCamera/addons/godot_rl_agents/sensors/sensors_2d/ISensor2D.gd +++ b/examples/VirtualCamera/addons/godot_rl_agents/sensors/sensors_2d/ISensor2D.gd @@ -1,20 +1,25 @@ extends Node2D class_name ISensor2D -var _obs : Array = [] +var _obs: Array = [] var _active := false + func get_observation(): pass - + + func activate(): _active = true - + + func deactivate(): _active = false + func _update_observation(): pass - + + func reset(): pass diff --git a/examples/VirtualCamera/addons/godot_rl_agents/sensors/sensors_2d/RaycastSensor2D.gd b/examples/VirtualCamera/addons/godot_rl_agents/sensors/sensors_2d/RaycastSensor2D.gd index 09363c4..9bb54ed 100644 --- a/examples/VirtualCamera/addons/godot_rl_agents/sensors/sensors_2d/RaycastSensor2D.gd +++ b/examples/VirtualCamera/addons/godot_rl_agents/sensors/sensors_2d/RaycastSensor2D.gd @@ -3,50 +3,57 @@ extends ISensor2D class_name RaycastSensor2D @export_flags_2d_physics var collision_mask := 1: - get: return collision_mask + get: + return collision_mask set(value): collision_mask = value _update() @export var collide_with_areas := false: - get: return collide_with_areas + get: + return collide_with_areas set(value): collide_with_areas = value _update() @export var collide_with_bodies := true: - get: return collide_with_bodies + get: + return collide_with_bodies set(value): collide_with_bodies = value _update() @export var n_rays := 16.0: - get: return n_rays + get: + return n_rays set(value): n_rays = value _update() - -@export_range(5,200,5.0) var ray_length := 200: - get: return ray_length + +@export_range(5, 3000, 5.0) var ray_length := 200: + get: + return ray_length set(value): ray_length = value _update() -@export_range(5,360,5.0) var cone_width := 360.0: - get: return cone_width +@export_range(5, 360, 5.0) var cone_width := 360.0: + get: + return cone_width set(value): cone_width = value _update() - -@export var debug_draw := true : - get: return debug_draw + +@export var debug_draw := true: + get: + return debug_draw set(value): debug_draw = value - _update() - + _update() var _angles = [] var rays := [] + func _update(): if Engine.is_editor_hint(): if debug_draw: @@ -56,63 +63,56 @@ func _update(): if ray is RayCast2D: remove_child(ray) + func _ready() -> void: _spawn_nodes() + func _spawn_nodes(): for ray in rays: ray.queue_free() rays = [] - + _angles = [] var step = cone_width / (n_rays) - var start = step/2 - cone_width/2 - + var start = step / 2 - cone_width / 2 + for i in n_rays: var angle = start + i * step var ray = RayCast2D.new() - ray.set_target_position(Vector2( - ray_length*cos(deg_to_rad(angle)), - ray_length*sin(deg_to_rad(angle)) - )) - ray.set_name("node_"+str(i)) - ray.enabled = true + ray.set_target_position( + Vector2(ray_length * cos(deg_to_rad(angle)), ray_length * sin(deg_to_rad(angle))) + ) + ray.set_name("node_" + str(i)) + ray.enabled = false ray.collide_with_areas = collide_with_areas ray.collide_with_bodies = collide_with_bodies ray.collision_mask = collision_mask add_child(ray) rays.append(ray) - - + _angles.append(start + i * step) - -func _physics_process(delta: float) -> void: - if self._active: - self._obs = calculate_raycasts() - + func get_observation() -> Array: - if len(self._obs) == 0: - print("obs was null, forcing raycast update") - return self.calculate_raycasts() - return self._obs - + return self.calculate_raycasts() + func calculate_raycasts() -> Array: var result = [] for ray in rays: + ray.enabled = true ray.force_raycast_update() var distance = _get_raycast_distance(ray) result.append(distance) + ray.enabled = false return result -func _get_raycast_distance(ray : RayCast2D) -> float : + +func _get_raycast_distance(ray: RayCast2D) -> float: if !ray.is_colliding(): return 0.0 - + var distance = (global_position - ray.get_collision_point()).length() distance = clamp(distance, 0.0, ray_length) return (ray_length - distance) / ray_length - - - diff --git a/examples/VirtualCamera/addons/godot_rl_agents/sensors/sensors_3d/GridSensor3D.gd b/examples/VirtualCamera/addons/godot_rl_agents/sensors/sensors_3d/GridSensor3D.gd index cfce8a8..03593cc 100644 --- a/examples/VirtualCamera/addons/godot_rl_agents/sensors/sensors_3d/GridSensor3D.gd +++ b/examples/VirtualCamera/addons/godot_rl_agents/sensors/sensors_3d/GridSensor3D.gd @@ -3,50 +3,58 @@ extends ISensor3D class_name GridSensor3D @export var debug_view := false: - get: return debug_view + get: + return debug_view set(value): debug_view = value _update() - + @export_flags_3d_physics var detection_mask := 0: - get: return detection_mask + get: + return detection_mask set(value): detection_mask = value _update() @export var collide_with_areas := false: - get: return collide_with_areas + get: + return collide_with_areas set(value): collide_with_areas = value _update() @export var collide_with_bodies := false: # NOTE! The sensor will not detect StaticBody3D, add an area to static bodies to detect them - get: return collide_with_bodies + get: + return collide_with_bodies set(value): collide_with_bodies = value _update() @export_range(0.1, 2, 0.1) var cell_width := 1.0: - get: return cell_width + get: + return cell_width set(value): cell_width = value _update() @export_range(0.1, 2, 0.1) var cell_height := 1.0: - get: return cell_height + get: + return cell_height set(value): cell_height = value - _update() + _update() @export_range(1, 21, 2, "or_greater") var grid_size_x := 3: - get: return grid_size_x + get: + return grid_size_x set(value): grid_size_x = value _update() @export_range(1, 21, 2, "or_greater") var grid_size_z := 3: - get: return grid_size_z + get: + return grid_size_z set(value): grid_size_z = value _update() @@ -59,95 +67,106 @@ var _n_layers_per_cell: int var _highlighted_box_material: StandardMaterial3D var _standard_box_material: StandardMaterial3D + func get_observation(): return _obs_buffer + func reset(): _obs_buffer.fill(0) + func _update(): if Engine.is_editor_hint(): if is_node_ready(): - _spawn_nodes() + _spawn_nodes() + func _ready() -> void: _make_materials() - - if Engine.is_editor_hint(): + + if Engine.is_editor_hint(): if get_child_count() == 0: _spawn_nodes() else: _spawn_nodes() - + + func _make_materials() -> void: if _highlighted_box_material != null and _standard_box_material != null: return - + _standard_box_material = StandardMaterial3D.new() - _standard_box_material.set_transparency(1) # ALPHA - _standard_box_material.albedo_color = Color(100.0/255.0, 100.0/255.0, 100.0/255.0, 100.0/255.0) - + _standard_box_material.set_transparency(1) # ALPHA + _standard_box_material.albedo_color = Color( + 100.0 / 255.0, 100.0 / 255.0, 100.0 / 255.0, 100.0 / 255.0 + ) + _highlighted_box_material = StandardMaterial3D.new() - _highlighted_box_material.set_transparency(1) # ALPHA - _highlighted_box_material.albedo_color = Color(255.0/255.0, 100.0/255.0, 100.0/255.0, 100.0/255.0) + _highlighted_box_material.set_transparency(1) # ALPHA + _highlighted_box_material.albedo_color = Color( + 255.0 / 255.0, 100.0 / 255.0, 100.0 / 255.0, 100.0 / 255.0 + ) + func _get_collision_mapping() -> Dictionary: # defines which layer is mapped to which cell obs index var total_bits = 0 - var collision_mapping = {} + var collision_mapping = {} for i in 32: - var bit_mask = 2**i + var bit_mask = 2 ** i if (detection_mask & bit_mask) > 0: collision_mapping[i] = total_bits total_bits += 1 - + return collision_mapping + func _spawn_nodes(): for cell in get_children(): - cell.name = "_%s" % cell.name # Otherwise naming below will fail + cell.name = "_%s" % cell.name # Otherwise naming below will fail cell.queue_free() - + _collision_mapping = _get_collision_mapping() #prints("collision_mapping", _collision_mapping, len(_collision_mapping)) # allocate memory for the observations _n_layers_per_cell = len(_collision_mapping) _obs_buffer = PackedFloat64Array() - _obs_buffer.resize(grid_size_x*grid_size_z*_n_layers_per_cell) + _obs_buffer.resize(grid_size_x * grid_size_z * _n_layers_per_cell) _obs_buffer.fill(0) #prints(len(_obs_buffer), _obs_buffer ) - + _box_shape = BoxShape3D.new() _box_shape.set_size(Vector3(cell_width, cell_height, cell_width)) - + var shift := Vector3( - -(grid_size_x/2)*cell_width, + -(grid_size_x / 2) * cell_width, 0, - -(grid_size_z/2)*cell_width, + -(grid_size_z / 2) * cell_width, ) - + for i in grid_size_x: for j in grid_size_z: - var cell_position = Vector3(i*cell_width, 0.0, j*cell_width) + shift + var cell_position = Vector3(i * cell_width, 0.0, j * cell_width) + shift _create_cell(i, j, cell_position) - -func _create_cell(i:int, j:int, position: Vector3): - var cell : = Area3D.new() + +func _create_cell(i: int, j: int, position: Vector3): + var cell := Area3D.new() cell.position = position - cell.name = "GridCell %s %s" %[i, j] - + cell.name = "GridCell %s %s" % [i, j] + if collide_with_areas: cell.area_entered.connect(_on_cell_area_entered.bind(i, j)) cell.area_exited.connect(_on_cell_area_exited.bind(i, j)) - + if collide_with_bodies: cell.body_entered.connect(_on_cell_body_entered.bind(i, j)) cell.body_exited.connect(_on_cell_body_exited.bind(i, j)) - + # cell.body_shape_entered.connect(_on_cell_body_shape_entered.bind(i, j)) # cell.body_shape_exited.connect(_on_cell_body_shape_exited.bind(i, j)) - + cell.collision_layer = 0 cell.collision_mask = detection_mask cell.monitorable = true @@ -155,78 +174,84 @@ func _create_cell(i:int, j:int, position: Vector3): add_child(cell) cell.set_owner(get_tree().edited_scene_root) - var col_shape : = CollisionShape3D.new() + var col_shape := CollisionShape3D.new() col_shape.shape = _box_shape col_shape.name = "CollisionShape3D" cell.add_child(col_shape) col_shape.set_owner(get_tree().edited_scene_root) - + if debug_view: var box = MeshInstance3D.new() box.name = "MeshInstance3D" var box_mesh = BoxMesh.new() - + box_mesh.set_size(Vector3(cell_width, cell_height, cell_width)) box_mesh.material = _standard_box_material - + box.mesh = box_mesh cell.add_child(box) box.set_owner(get_tree().edited_scene_root) -func _update_obs(cell_i:int, cell_j:int, collision_layer:int, entered: bool): + +func _update_obs(cell_i: int, cell_j: int, collision_layer: int, entered: bool): for key in _collision_mapping: - var bit_mask = 2**key + var bit_mask = 2 ** key if (collision_layer & bit_mask) > 0: var collison_map_index = _collision_mapping[key] - + var obs_index = ( - (cell_i * grid_size_x * _n_layers_per_cell) + - (cell_j * _n_layers_per_cell) + - collison_map_index - ) + (cell_i * grid_size_x * _n_layers_per_cell) + + (cell_j * _n_layers_per_cell) + + collison_map_index + ) #prints(obs_index, cell_i, cell_j) if entered: _obs_buffer[obs_index] += 1 else: _obs_buffer[obs_index] -= 1 -func _toggle_cell(cell_i:int, cell_j:int): - var cell = get_node_or_null("GridCell %s %s" %[cell_i, cell_j]) - + +func _toggle_cell(cell_i: int, cell_j: int): + var cell = get_node_or_null("GridCell %s %s" % [cell_i, cell_j]) + if cell == null: print("cell not found, returning") - + var n_hits = 0 var start_index = (cell_i * grid_size_x * _n_layers_per_cell) + (cell_j * _n_layers_per_cell) for i in _n_layers_per_cell: - n_hits += _obs_buffer[start_index+i] - + n_hits += _obs_buffer[start_index + i] + var cell_mesh = cell.get_node_or_null("MeshInstance3D") if n_hits > 0: cell_mesh.mesh.material = _highlighted_box_material else: cell_mesh.mesh.material = _standard_box_material - -func _on_cell_area_entered(area:Area3D, cell_i:int, cell_j:int): + + +func _on_cell_area_entered(area: Area3D, cell_i: int, cell_j: int): #prints("_on_cell_area_entered", cell_i, cell_j) _update_obs(cell_i, cell_j, area.collision_layer, true) if debug_view: _toggle_cell(cell_i, cell_j) #print(_obs_buffer) -func _on_cell_area_exited(area:Area3D, cell_i:int, cell_j:int): + +func _on_cell_area_exited(area: Area3D, cell_i: int, cell_j: int): #prints("_on_cell_area_exited", cell_i, cell_j) _update_obs(cell_i, cell_j, area.collision_layer, false) if debug_view: _toggle_cell(cell_i, cell_j) -func _on_cell_body_entered(body: Node3D, cell_i:int, cell_j:int): + +func _on_cell_body_entered(body: Node3D, cell_i: int, cell_j: int): #prints("_on_cell_body_entered", cell_i, cell_j) _update_obs(cell_i, cell_j, body.collision_layer, true) if debug_view: _toggle_cell(cell_i, cell_j) -func _on_cell_body_exited(body: Node3D, cell_i:int, cell_j:int): + +func _on_cell_body_exited(body: Node3D, cell_i: int, cell_j: int): #prints("_on_cell_body_exited", cell_i, cell_j) _update_obs(cell_i, cell_j, body.collision_layer, false) if debug_view: diff --git a/examples/VirtualCamera/addons/godot_rl_agents/sensors/sensors_3d/ISensor3D.gd b/examples/VirtualCamera/addons/godot_rl_agents/sensors/sensors_3d/ISensor3D.gd index d57503b..aca3c2d 100644 --- a/examples/VirtualCamera/addons/godot_rl_agents/sensors/sensors_3d/ISensor3D.gd +++ b/examples/VirtualCamera/addons/godot_rl_agents/sensors/sensors_3d/ISensor3D.gd @@ -1,20 +1,25 @@ extends Node3D class_name ISensor3D -var _obs : Array = [] +var _obs: Array = [] var _active := false + func get_observation(): pass - + + func activate(): _active = true - + + func deactivate(): _active = false + func _update_observation(): pass - + + func reset(): pass diff --git a/examples/VirtualCamera/addons/godot_rl_agents/sensors/sensors_3d/RGBCameraSensor3D.gd b/examples/VirtualCamera/addons/godot_rl_agents/sensors/sensors_3d/RGBCameraSensor3D.gd index 1037e97..78bcbf8 100644 --- a/examples/VirtualCamera/addons/godot_rl_agents/sensors/sensors_3d/RGBCameraSensor3D.gd +++ b/examples/VirtualCamera/addons/godot_rl_agents/sensors/sensors_3d/RGBCameraSensor3D.gd @@ -2,20 +2,66 @@ extends Node3D class_name RGBCameraSensor3D var camera_pixels = null -@onready var camera_texture := $Control/TextureRect/CameraTexture as Sprite2D +@onready var camera_texture := $Control/CameraTexture as Sprite2D +@onready var processed_texture := $Control/ProcessedTexture as Sprite2D @onready var sub_viewport := $SubViewport as SubViewport +@onready var displayed_image: ImageTexture + +## We need to encode the image differently when training or running inference +@export var training_mode: bool + +@export var render_image_resolution := Vector2(36, 36) +## Display size does not affect rendered or sent image resolution. +## Scale is relative to either render image or downscale image resolution +## depending on which mode is set. +@export var displayed_image_scale_factor := Vector2(8, 8) + +@export_group("Downscale image options") +## Enable to downscale the rendered image before sending the obs. +@export var downscale_image: bool = false +## If downscale_image is true, will display the downscaled image instead of rendered image. +@export var display_downscaled_image: bool = true +## This is the resolution of the image that will be sent after downscaling +@export var resized_image_resolution := Vector2(36, 36) + + +func _ready(): + sub_viewport.size = render_image_resolution + camera_texture.scale = displayed_image_scale_factor + + if downscale_image and display_downscaled_image: + camera_texture.visible = false + processed_texture.scale = displayed_image_scale_factor + else: + processed_texture.visible = false func get_camera_pixel_encoding(): - return camera_texture.get_texture().get_image().get_data().hex_encode() + var image := camera_texture.get_texture().get_image() as Image + + if downscale_image: + image.resize( + resized_image_resolution.x, resized_image_resolution.y, Image.INTERPOLATE_NEAREST + ) + if display_downscaled_image: + if not processed_texture.texture: + displayed_image = ImageTexture.create_from_image(image) + processed_texture.texture = displayed_image + else: + displayed_image.update(image) + + var results = image.get_data().hex_encode() if training_mode else image.get_data() + return results func get_camera_shape() -> Array: - assert( - sub_viewport.size.x >= 36 and sub_viewport.size.y >= 36, - "SubViewport size must be 36x36 or larger." - ) + var size = resized_image_resolution if downscale_image else render_image_resolution + + #assert( + #size.x >= 36 and size.y >= 36, + #"Camera sensor sent image resolution must be 36x36 or larger." + #) if sub_viewport.transparent_bg: - return [4, sub_viewport.size.y, sub_viewport.size.x] + return [size.y, size.x, 4] else: - return [3, sub_viewport.size.y, sub_viewport.size.x] + return [size.y, size.x, 3] diff --git a/examples/VirtualCamera/addons/godot_rl_agents/sensors/sensors_3d/RGBCameraSensor3D.tscn b/examples/VirtualCamera/addons/godot_rl_agents/sensors/sensors_3d/RGBCameraSensor3D.tscn index 052b557..d58649c 100644 --- a/examples/VirtualCamera/addons/godot_rl_agents/sensors/sensors_3d/RGBCameraSensor3D.tscn +++ b/examples/VirtualCamera/addons/godot_rl_agents/sensors/sensors_3d/RGBCameraSensor3D.tscn @@ -2,20 +2,20 @@ [ext_resource type="Script" path="res://addons/godot_rl_agents/sensors/sensors_3d/RGBCameraSensor3D.gd" id="1"] -[sub_resource type="ViewportTexture" id="1"] +[sub_resource type="ViewportTexture" id="ViewportTexture_y72s3"] viewport_path = NodePath("SubViewport") [node name="RGBCameraSensor3D" type="Node3D"] script = ExtResource("1") -[node name="RemoteTransform3D" type="RemoteTransform3D" parent="."] -remote_path = NodePath("../SubViewport/Camera3D") +[node name="RemoteTransform" type="RemoteTransform3D" parent="."] +remote_path = NodePath("../SubViewport/Camera") [node name="SubViewport" type="SubViewport" parent="."] -size = Vector2i(32, 32) +size = Vector2i(36, 36) render_target_update_mode = 3 -[node name="Camera3D" type="Camera3D" parent="SubViewport"] +[node name="Camera" type="Camera3D" parent="SubViewport"] near = 0.5 [node name="Control" type="Control" parent="."] @@ -25,17 +25,11 @@ anchor_right = 1.0 anchor_bottom = 1.0 grow_horizontal = 2 grow_vertical = 2 +metadata/_edit_use_anchors_ = true -[node name="TextureRect" type="ColorRect" parent="Control"] -layout_mode = 0 -offset_left = 1096.0 -offset_top = 534.0 -offset_right = 1114.0 -offset_bottom = 552.0 -scale = Vector2(10, 10) -color = Color(0.00784314, 0.00784314, 0.00784314, 1) - -[node name="CameraTexture" type="Sprite2D" parent="Control/TextureRect"] -texture = SubResource("1") -offset = Vector2(9, 9) -flip_v = true +[node name="CameraTexture" type="Sprite2D" parent="Control"] +texture = SubResource("ViewportTexture_y72s3") +centered = false + +[node name="ProcessedTexture" type="Sprite2D" parent="Control"] +centered = false diff --git a/examples/VirtualCamera/addons/godot_rl_agents/sensors/sensors_3d/RaycastSensor3D.gd b/examples/VirtualCamera/addons/godot_rl_agents/sensors/sensors_3d/RaycastSensor3D.gd index 1f36193..1357529 100644 --- a/examples/VirtualCamera/addons/godot_rl_agents/sensors/sensors_3d/RaycastSensor3D.gd +++ b/examples/VirtualCamera/addons/godot_rl_agents/sensors/sensors_3d/RaycastSensor3D.gd @@ -2,70 +2,86 @@ extends ISensor3D class_name RayCastSensor3D @export_flags_3d_physics var collision_mask = 1: - get: return collision_mask + get: + return collision_mask set(value): collision_mask = value _update() @export_flags_3d_physics var boolean_class_mask = 1: - get: return boolean_class_mask + get: + return boolean_class_mask set(value): boolean_class_mask = value _update() @export var n_rays_width := 6.0: - get: return n_rays_width + get: + return n_rays_width set(value): n_rays_width = value _update() - + @export var n_rays_height := 6.0: - get: return n_rays_height + get: + return n_rays_height set(value): n_rays_height = value _update() @export var ray_length := 10.0: - get: return ray_length + get: + return ray_length set(value): ray_length = value _update() - + @export var cone_width := 60.0: - get: return cone_width + get: + return cone_width set(value): cone_width = value _update() - + @export var cone_height := 60.0: - get: return cone_height + get: + return cone_height set(value): cone_height = value _update() @export var collide_with_areas := false: - get: return collide_with_areas + get: + return collide_with_areas set(value): collide_with_areas = value _update() - + @export var collide_with_bodies := true: - get: return collide_with_bodies + get: + return collide_with_bodies set(value): collide_with_bodies = value _update() @export var class_sensor := false - + var rays := [] var geo = null + func _update(): if Engine.is_editor_hint(): - _spawn_nodes() + if is_node_ready(): + _spawn_nodes() func _ready() -> void: - _spawn_nodes() + if Engine.is_editor_hint(): + if get_child_count() == 0: + _spawn_nodes() + else: + _spawn_nodes() + func _spawn_nodes(): print("spawning nodes") @@ -75,15 +91,15 @@ func _spawn_nodes(): geo.clear() #$Lines.remove_points() rays = [] - + var horizontal_step = cone_width / (n_rays_width) var vertical_step = cone_height / (n_rays_height) - - var horizontal_start = horizontal_step/2 - cone_width/2 - var vertical_start = vertical_step/2 - cone_height/2 + + var horizontal_start = horizontal_step / 2 - cone_width / 2 + var vertical_start = vertical_step / 2 - cone_height / 2 var points = [] - + for i in n_rays_width: for j in n_rays_height: var angle_w = horizontal_start + i * horizontal_step @@ -94,9 +110,9 @@ func _spawn_nodes(): ray.set_target_position(cast_to) points.append(cast_to) - - ray.set_name("node_"+str(i)+" "+str(j)) - ray.enabled = true + + ray.set_name("node_" + str(i) + " " + str(j)) + ray.enabled = true ray.collide_with_bodies = collide_with_bodies ray.collide_with_areas = collide_with_areas ray.collision_mask = collision_mask @@ -104,15 +120,17 @@ func _spawn_nodes(): ray.set_owner(get_tree().edited_scene_root) rays.append(ray) ray.force_raycast_update() - + + # if Engine.editor_hint: # _create_debug_lines(points) - + + func _create_debug_lines(points): - if not geo: + if not geo: geo = ImmediateMesh.new() add_child(geo) - + geo.clear() geo.begin(Mesh.PRIMITIVE_LINES) for point in points: @@ -121,20 +139,24 @@ func _create_debug_lines(points): geo.add_vertex(point) geo.end() + func display(): if geo: geo.display() - + + func to_spherical_coords(r, inc, azimuth) -> Vector3: return Vector3( - r*sin(deg_to_rad(inc))*cos(deg_to_rad(azimuth)), - r*sin(deg_to_rad(azimuth)), - r*cos(deg_to_rad(inc))*cos(deg_to_rad(azimuth)) + r * sin(deg_to_rad(inc)) * cos(deg_to_rad(azimuth)), + r * sin(deg_to_rad(azimuth)), + r * cos(deg_to_rad(inc)) * cos(deg_to_rad(azimuth)) ) - + + func get_observation() -> Array: return self.calculate_raycasts() + func calculate_raycasts() -> Array: var result = [] for ray in rays: @@ -144,19 +166,20 @@ func calculate_raycasts() -> Array: result.append(distance) if class_sensor: - var hit_class = 0 + var hit_class: float = 0 if ray.get_collider(): var hit_collision_layer = ray.get_collider().collision_layer hit_collision_layer = hit_collision_layer & collision_mask hit_class = (hit_collision_layer & boolean_class_mask) > 0 - result.append(hit_class) + result.append(float(hit_class)) ray.set_enabled(false) return result -func _get_raycast_distance(ray : RayCast3D) -> float : + +func _get_raycast_distance(ray: RayCast3D) -> float: if !ray.is_colliding(): return 0.0 - + var distance = (global_transform.origin - ray.get_collision_point()).length() distance = clamp(distance, 0.0, ray_length) return (ray_length - distance) / ray_length diff --git a/examples/VirtualCamera/addons/godot_rl_agents/sync.gd b/examples/VirtualCamera/addons/godot_rl_agents/sync.gd index 884e4e4..8e43039 100644 --- a/examples/VirtualCamera/addons/godot_rl_agents/sync.gd +++ b/examples/VirtualCamera/addons/godot_rl_agents/sync.gd @@ -1,20 +1,43 @@ extends Node + # --fixed-fps 2000 --disable-render-loop + +enum ControlModes { HUMAN, TRAINING, ONNX_INFERENCE } +@export var control_mode: ControlModes = ControlModes.TRAINING @export_range(1, 10, 1, "or_greater") var action_repeat := 8 -@export_range(1, 10, 1, "or_greater") var speed_up = 1 +@export_range(0, 10, 0.1, "or_greater") var speed_up := 1.0 @export var onnx_model_path := "" +# Onnx model stored for each requested path +var onnx_models: Dictionary + @onready var start_time = Time.get_ticks_msec() const MAJOR_VERSION := "0" -const MINOR_VERSION := "3" +const MINOR_VERSION := "7" const DEFAULT_PORT := "11008" const DEFAULT_SEED := "1" -var stream : StreamPeerTCP = null +var stream: StreamPeerTCP = null var connected = false var message_center var should_connect = true -var agents + +var all_agents: Array +var agents_training: Array +## Policy name of each agent, for use with multi-policy multi-agent RL cases +var agents_training_policy_names: Array[String] = ["shared_policy"] +var agents_inference: Array +var agents_heuristic: Array + +## For recording expert demos +var agent_demo_record: Node +## File path for writing recorded trajectories +var expert_demo_save_path: String +## Stores recorded trajectories +var demo_trajectories: Array +## A trajectory includes obs: Array, acts: Array, terminal (set in Python env instead) +var current_demo_trajectory: Array + var need_to_send_obs = false var args = null var initialized = false @@ -22,141 +45,329 @@ var just_reset = false var onnx_model = null var n_action_steps = 0 -var _action_space : Dictionary -var _obs_space : Dictionary +var _action_space_training: Array[Dictionary] = [] +var _action_space_inference: Array[Dictionary] = [] +var _obs_space_training: Array[Dictionary] = [] # Called when the node enters the scene tree for the first time. - func _ready(): await get_tree().root.ready - get_tree().set_pause(true) + get_tree().set_pause(true) _initialize() await get_tree().create_timer(1.0).timeout - get_tree().set_pause(false) - + get_tree().set_pause(false) + + func _initialize(): _get_agents() - _obs_space = agents[0].get_obs_space() - _action_space = agents[0].get_action_space() args = _get_args() - Engine.physics_ticks_per_second = _get_speedup() * 60 # Replace with function body. + Engine.physics_ticks_per_second = _get_speedup() * 60 # Replace with function body. Engine.time_scale = _get_speedup() * 1.0 - prints("physics ticks", Engine.physics_ticks_per_second, Engine.time_scale, _get_speedup(), speed_up) - - # Run inference if onnx model path is set, otherwise wait for server connection - var run_onnx_model_inference : bool = onnx_model_path != "" - if run_onnx_model_inference: - assert(FileAccess.file_exists(onnx_model_path), "Onnx Model Path set on Sync node does not exist: " + onnx_model_path) - onnx_model = ONNXModel.new(onnx_model_path, 1) - _set_heuristic("model") - else: + prints( + "physics ticks", + Engine.physics_ticks_per_second, + Engine.time_scale, + _get_speedup(), + speed_up + ) + + _set_heuristic("human", all_agents) + + _initialize_training_agents() + _initialize_inference_agents() + _initialize_demo_recording() + + _set_seed() + _set_action_repeat() + initialized = true + + +func _initialize_training_agents(): + if agents_training.size() > 0: + _obs_space_training.resize(agents_training.size()) + _action_space_training.resize(agents_training.size()) + for agent_idx in range(0, agents_training.size()): + _obs_space_training[agent_idx] = agents_training[agent_idx].get_obs_space() + _action_space_training[agent_idx] = agents_training[agent_idx].get_action_space() connected = connect_to_server() if connected: - _set_heuristic("model") + _set_heuristic("model", agents_training) _handshake() _send_env_info() else: - _set_heuristic("human") - - _set_seed() - _set_action_repeat() - initialized = true - -func _physics_process(delta): + push_warning( + "Couldn't connect to Python server, using human controls instead. ", + "Did you start the training server using e.g. `gdrl` from the console?" + ) + + +func _initialize_inference_agents(): + if agents_inference.size() > 0: + if control_mode == ControlModes.ONNX_INFERENCE: + assert( + FileAccess.file_exists(onnx_model_path), + "Onnx Model Path set on Sync node does not exist: %s" % onnx_model_path + ) + onnx_models[onnx_model_path] = ONNXModel.new(onnx_model_path, 1) + + for agent in agents_inference: + var action_space = agent.get_action_space() + _action_space_inference.append(action_space) + + var agent_onnx_model: ONNXModel + if agent.onnx_model_path.is_empty(): + assert( + onnx_models.has(onnx_model_path), + ( + "Node %s has no onnx model path set " % agent.get_path() + + "and sync node's control mode is not set to OnnxInference. " + + "Either add the path to the AIController, " + + "or if you want to use the path set on sync node instead, " + + "set control mode to OnnxInference." + ) + ) + prints( + "Info: AIController %s" % agent.get_path(), + "has no onnx model path set.", + "Using path set on the sync node instead." + ) + agent_onnx_model = onnx_models[onnx_model_path] + else: + if not onnx_models.has(agent.onnx_model_path): + assert( + FileAccess.file_exists(agent.onnx_model_path), + ( + "Onnx Model Path set on %s node does not exist: %s" + % [agent.get_path(), agent.onnx_model_path] + ) + ) + onnx_models[agent.onnx_model_path] = ONNXModel.new(agent.onnx_model_path, 1) + agent_onnx_model = onnx_models[agent.onnx_model_path] + + agent.onnx_model = agent_onnx_model + if not agent_onnx_model.action_means_only_set: + agent_onnx_model.set_action_means_only(action_space) + + _set_heuristic("model", agents_inference) + + +func _initialize_demo_recording(): + if agent_demo_record: + expert_demo_save_path = agent_demo_record.expert_demo_save_path + assert( + not expert_demo_save_path.is_empty(), + "Expert demo save path set in %s is empty." % agent_demo_record.get_path() + ) + + InputMap.add_action("RemoveLastDemoEpisode") + InputMap.action_add_event( + "RemoveLastDemoEpisode", agent_demo_record.remove_last_episode_key + ) + current_demo_trajectory.resize(2) + current_demo_trajectory[0] = [] + current_demo_trajectory[1] = [] + agent_demo_record.heuristic = "demo_record" + + +func _physics_process(_delta): # two modes, human control, agent control # pause tree, send obs, get actions, set actions, unpause tree + + _demo_record_process() + if n_action_steps % action_repeat != 0: n_action_steps += 1 return n_action_steps += 1 - + + _training_process() + _inference_process() + _heuristic_process() + + +func _training_process(): if connected: - get_tree().set_pause(true) - + get_tree().set_pause(true) + if just_reset: just_reset = false - var obs = _get_obs_from_agents() - - var reply = { - "type": "reset", - "obs": obs - } + var obs = _get_obs_from_agents(agents_training) + + var reply = {"type": "reset", "obs": obs} _send_dict_as_json_message(reply) # this should go straight to getting the action and setting it checked the agent, no need to perform one phyics tick - get_tree().set_pause(false) + get_tree().set_pause(false) return - + if need_to_send_obs: need_to_send_obs = false var reward = _get_reward_from_agents() var done = _get_done_from_agents() #_reset_agents_if_done() # this ensures the new observation is from the next env instance : NEEDS REFACTOR - - var obs = _get_obs_from_agents() - - var reply = { - "type": "step", - "obs": obs, - "reward": reward, - "done": done - } + + var obs = _get_obs_from_agents(agents_training) + + var reply = {"type": "step", "obs": obs, "reward": reward, "done": done} _send_dict_as_json_message(reply) - + var handled = handle_message() - - elif onnx_model != null: - var obs : Array = _get_obs_from_agents() - + + +func _inference_process(): + if agents_inference.size() > 0: + var obs: Array = _get_obs_from_agents(agents_inference) var actions = [] - for o in obs: - var action = onnx_model.run_inference(o["obs"], 1.0) - action["output"] = clamp_array(action["output"], -1.0, 1.0) - var action_dict = _extract_action_dict(action["output"]) + + for agent_id in range(0, agents_inference.size()): + var model: ONNXModel = agents_inference[agent_id].onnx_model + var action = model.run_inference( + obs[agent_id]["camera_2d"], 1.0 + ) + var action_dict = _extract_action_dict( + action["output"], _action_space_inference[agent_id], model.action_means_only + ) actions.append(action_dict) - - _set_agent_actions(actions) - need_to_send_obs = true - get_tree().set_pause(false) - _reset_agents_if_done() - + + _set_agent_actions(actions, agents_inference) + _reset_agents_if_done(agents_inference) + get_tree().set_pause(false) + + +func _demo_record_process(): + if not agent_demo_record: + return + + if Input.is_action_just_pressed("RemoveLastDemoEpisode"): + print("[Sync script][Demo recorder] Removing last recorded episode.") + demo_trajectories.remove_at(demo_trajectories.size() - 1) + print("Remaining episode count: %d" % demo_trajectories.size()) + + if n_action_steps % agent_demo_record.action_repeat != 0: + return + + var obs_dict: Dictionary = agent_demo_record.get_obs() + + # Get the current obs from the agent + assert( + obs_dict.has("obs"), + "Demo recorder needs an 'obs' key in get_obs() returned dictionary to record obs from." + ) + current_demo_trajectory[0].append(obs_dict.obs) + + # Get the action applied for the current obs from the agent + agent_demo_record.set_action() + var acts = agent_demo_record.get_action() + + var terminal = agent_demo_record.get_done() + # Record actions only for non-terminal states + if terminal: + agent_demo_record.set_done_false() else: - _reset_agents_if_done() + current_demo_trajectory[1].append(acts) -func _extract_action_dict(action_array: Array): + if terminal: + #current_demo_trajectory[2].append(true) + demo_trajectories.append(current_demo_trajectory.duplicate(true)) + print("[Sync script][Demo recorder] Recorded episode count: %d" % demo_trajectories.size()) + current_demo_trajectory[0].clear() + current_demo_trajectory[1].clear() + + +func _heuristic_process(): + for agent in agents_heuristic: + _reset_agents_if_done(agents_heuristic) + + +func _extract_action_dict(action_array: Array, action_space: Dictionary, action_means_only: bool): var index = 0 var result = {} - for key in _action_space.keys(): - var size = _action_space[key]["size"] - if _action_space[key]["action_type"] == "discrete": - result[key] = round(action_array[index]) + for key in action_space.keys(): + var size = action_space[key]["size"] + var action_type = action_space[key]["action_type"] + if action_type == "discrete": + var largest_logit: float # Value of the largest logit for this action in the actions array + var largest_logit_idx: int # Index of the largest logit for this action in the actions array + for logit_idx in range(0, size): + var logit_value = action_array[index + logit_idx] + if logit_value > largest_logit: + largest_logit = logit_value + largest_logit_idx = logit_idx + result[key] = largest_logit_idx # Index of the largest logit is the discrete action value + index += size + elif action_type == "continuous": + # For continous actions, we only take the action mean values + result[key] = clamp_array(action_array.slice(index, index + size), -1.0, 1.0) + if action_means_only: + index += size # model only outputs action means, so we move index by size + else: + index += size * 2 # model outputs logstd after action mean, we skip the logstd part + else: - result[key] = action_array.slice(index,index+size) - index += size + assert(false, 'Only "discrete" and "continuous" action types supported. Found: %s action type set.' % action_type) + return result + +## For AIControllers that inherit mode from sync, sets the correct mode. +func _set_agent_mode(agent: Node): + var agent_inherits_mode: bool = agent.control_mode == agent.ControlModes.INHERIT_FROM_SYNC + + if agent_inherits_mode: + match control_mode: + ControlModes.HUMAN: + agent.control_mode = agent.ControlModes.HUMAN + ControlModes.TRAINING: + agent.control_mode = agent.ControlModes.TRAINING + ControlModes.ONNX_INFERENCE: + agent.control_mode = agent.ControlModes.ONNX_INFERENCE + + func _get_agents(): - agents = get_tree().get_nodes_in_group("AGENT") + all_agents = get_tree().get_nodes_in_group("AGENT") + for agent in all_agents: + _set_agent_mode(agent) + + if agent.control_mode == agent.ControlModes.TRAINING: + agents_training.append(agent) + elif agent.control_mode == agent.ControlModes.ONNX_INFERENCE: + agents_inference.append(agent) + elif agent.control_mode == agent.ControlModes.HUMAN: + agents_heuristic.append(agent) + elif agent.control_mode == agent.ControlModes.RECORD_EXPERT_DEMOS: + assert( + not agent_demo_record, + "Currently only a single AIController can be used for recording expert demos." + ) + agent_demo_record = agent + + var training_agent_count = agents_training.size() + agents_training_policy_names.resize(training_agent_count) + for i in range(0, training_agent_count): + agents_training_policy_names[i] = agents_training[i].policy_name + -func _set_heuristic(heuristic): +func _set_heuristic(heuristic, agents: Array): for agent in agents: agent.set_heuristic(heuristic) + func _handshake(): print("performing handshake") - + var json_dict = _get_dict_json_message() assert(json_dict["type"] == "handshake") var major_version = json_dict["major_version"] var minor_version = json_dict["minor_version"] if major_version != MAJOR_VERSION: - print("WARNING: major verison mismatch ", major_version, " ", MAJOR_VERSION) + print("WARNING: major verison mismatch ", major_version, " ", MAJOR_VERSION) if minor_version != MINOR_VERSION: print("WARNING: minor verison mismatch ", minor_version, " ", MINOR_VERSION) - + print("handshake complete") + func _get_dict_json_message(): # returns a dictionary from of the most recent message # this is not waiting @@ -168,45 +379,49 @@ func _get_dict_json_message(): return null OS.delay_usec(10) - + var message = stream.get_string() var json_data = JSON.parse_string(message) - + return json_data + func _send_dict_as_json_message(dict): - stream.put_string(JSON.stringify(dict)) + stream.put_string(JSON.stringify(dict, "", false)) + func _send_env_info(): var json_dict = _get_dict_json_message() assert(json_dict["type"] == "env_info") - var message = { - "type" : "env_info", - "observation_space": _obs_space, - "action_space":_action_space, - "n_agents": len(agents) - } + "type": "env_info", + "observation_space": _obs_space_training, + "action_space": _action_space_training, + "n_agents": len(agents_training), + "agent_policy_names": agents_training_policy_names + } _send_dict_as_json_message(message) + func connect_to_server(): print("Waiting for one second to allow server to start") OS.delay_msec(1000) print("trying to connect to server") stream = StreamPeerTCP.new() - + # "localhost" was not working on windows VM, had to use the IP var ip = "127.0.0.1" var port = _get_port() var connect = stream.connect_to_host(ip, port) - stream.set_no_delay(true) # TODO check if this improves performance or not + stream.set_no_delay(true) # TODO check if this improves performance or not stream.poll() # Fetch the status until it is either connected (2) or failed to connect (3) while stream.get_status() < 2: stream.poll() return stream.get_status() == 2 + func _get_args(): print("getting command line arguments") var arguments = {} @@ -220,41 +435,45 @@ func _get_args(): # with the value set to an empty string. arguments[argument.lstrip("--")] = "" - return arguments + return arguments + func _get_speedup(): print(args) - return args.get("speedup", str(speed_up)).to_int() + return args.get("speedup", str(speed_up)).to_float() -func _get_port(): + +func _get_port(): return args.get("port", DEFAULT_PORT).to_int() + func _set_seed(): var _seed = args.get("env_seed", DEFAULT_SEED).to_int() seed(_seed) + func _set_action_repeat(): action_repeat = args.get("action_repeat", str(action_repeat)).to_int() - + + func disconnect_from_server(): stream.disconnect_from_host() - func handle_message() -> bool: # get json message: reset, step, close var message = _get_dict_json_message() if message["type"] == "close": print("received close message, closing game") get_tree().quit() - get_tree().set_pause(false) + get_tree().set_pause(false) return true - + if message["type"] == "reset": print("resetting all agents") - _reset_all_agents() + _reset_agents() just_reset = true - get_tree().set_pause(false) + get_tree().set_pause(false) #print("resetting forcing draw") # RenderingServer.force_draw() # var obs = _get_obs_from_agents() @@ -263,76 +482,98 @@ func handle_message() -> bool: # "type": "reset", # "obs": obs # } -# _send_dict_as_json_message(reply) +# _send_dict_as_json_message(reply) return true - + if message["type"] == "call": var method = message["method"] var returns = _call_method_on_agents(method) - var reply = { - "type": "call", - "returns": returns - } + var reply = {"type": "call", "returns": returns} print("calling method from Python") - _send_dict_as_json_message(reply) + _send_dict_as_json_message(reply) return handle_message() - + if message["type"] == "action": var action = message["action"] - _set_agent_actions(action) + _set_agent_actions(action, agents_training) need_to_send_obs = true - get_tree().set_pause(false) + get_tree().set_pause(false) return true - + print("message was not handled") return false + func _call_method_on_agents(method): var returns = [] - for agent in agents: + for agent in all_agents: returns.append(agent.call(method)) - + return returns -func _reset_agents_if_done(): +func _reset_agents_if_done(agents = all_agents): for agent in agents: - if agent.get_done(): + if agent.get_done(): agent.set_done_false() -func _reset_all_agents(): + +func _reset_agents(agents = all_agents): for agent in agents: agent.needs_reset = true - #agent.reset() + #agent.reset() -func _get_obs_from_agents(): + +func _get_obs_from_agents(agents: Array = all_agents): var obs = [] for agent in agents: obs.append(agent.get_obs()) - return obs - -func _get_reward_from_agents(): - var rewards = [] + + +func _get_reward_from_agents(agents: Array = agents_training): + var rewards = [] for agent in agents: rewards.append(agent.get_reward()) agent.zero_reward() - return rewards - -func _get_done_from_agents(): - var dones = [] + return rewards + + +func _get_done_from_agents(agents: Array = agents_training): + var dones = [] for agent in agents: var done = agent.get_done() - if done: agent.set_done_false() + if done: + agent.set_done_false() dones.append(done) - return dones - -func _set_agent_actions(actions): + return dones + + +func _set_agent_actions(actions, agents: Array = all_agents): for i in range(len(actions)): agents[i].set_action(actions[i]) - -func clamp_array(arr : Array, min:float, max:float): - var output : Array = [] + + +func clamp_array(arr: Array, min: float, max: float): + var output: Array = [] for a in arr: output.append(clamp(a, min, max)) return output + + +## Save recorded export demos on window exit (Close game window instead of "Stop" button in Godot Editor) +func _notification(what): + if demo_trajectories.size() == 0 or expert_demo_save_path.is_empty(): + return + + if what == NOTIFICATION_PREDELETE: + var json_string = JSON.stringify(demo_trajectories, "", false) + var file = FileAccess.open(expert_demo_save_path, FileAccess.WRITE) + + if not file: + var error: Error = FileAccess.get_open_error() + assert(not error, "There was an error opening the file: %d" % error) + + file.store_line(json_string) + var error = file.get_error() + assert(not error, "There was an error after trying to write to the file: %d" % error) diff --git a/examples/VirtualCamera/project.godot b/examples/VirtualCamera/project.godot index fbcff6e..2c53044 100644 --- a/examples/VirtualCamera/project.godot +++ b/examples/VirtualCamera/project.godot @@ -12,7 +12,7 @@ config_version=5 config/name="VirtualCamera" run/main_scene="res://Env.tscn" -config/features=PackedStringArray("4.2") +config/features=PackedStringArray("4.3", "C#") config/icon="res://icon.png" [display] @@ -32,27 +32,27 @@ enabled=PackedStringArray("res://addons/godot_rl_agents/plugin.cfg") turn_left={ "deadzone": 0.5, -"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":65,"physical_keycode":0,"key_label":0,"unicode":97,"echo":false,"script":null) +"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":65,"physical_keycode":0,"key_label":0,"unicode":97,"location":0,"echo":false,"script":null) ] } turn_right={ "deadzone": 0.5, -"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":68,"physical_keycode":0,"key_label":0,"unicode":100,"echo":false,"script":null) +"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":68,"physical_keycode":0,"key_label":0,"unicode":100,"location":0,"echo":false,"script":null) ] } move_forwards={ "deadzone": 0.5, -"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":87,"physical_keycode":0,"key_label":0,"unicode":119,"echo":false,"script":null) +"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":87,"physical_keycode":0,"key_label":0,"unicode":119,"location":0,"echo":false,"script":null) ] } move_backwards={ "deadzone": 0.5, -"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":83,"physical_keycode":0,"key_label":0,"unicode":115,"echo":false,"script":null) +"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":83,"physical_keycode":0,"key_label":0,"unicode":115,"location":0,"echo":false,"script":null) ] } r_key={ "deadzone": 0.5, -"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":82,"physical_keycode":0,"key_label":0,"unicode":114,"echo":false,"script":null) +"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":82,"physical_keycode":0,"key_label":0,"unicode":114,"location":0,"echo":false,"script":null) ] }