robolib.systems
Generic systems of robolib. Should have no depenendices on each other and operate on top of ECS components. Import level: 3
1""" 2Generic systems of robolib. Should have no depenendices on each other and operate on top of ECS components. 3Import level: 3 4""" 5 6from .render_system import RenderSystem 7from .physics_system import PhysicsSystem 8from .fpv_render_system import FPVRenderSystem 9 10__all__ = ["RenderSystem", "PhysicsSystem", "FPVRenderSystem"]
class
RenderSystem:
13class RenderSystem: 14 """Only the main 3D render camera(s). For FPV rendering on a texture, use FPVRenderSystem.""" 15 def __call__(self, world: World, camera: Camera, extra_drawables: list[Drawable] | None = None, 16 camera_entity_id: EntityId | None = None, wireframe_mode: bool = False, 17 collision_mode: bool = False, display_uav_trace: bool = False, uav_traces: dict | None = None, 18 display_hud: bool = False, extra_callables: list[Callable] | None = None): 19 rl.ClearBackground(DEFAULT_BACKGROUND) 20 21 rl.BeginMode3D(camera.camera[0]) 22 23 if wireframe_mode: 24 rl.rlEnableWireMode() 25 26 if collision_mode: 27 qr = world.query(HasModel, HasPose, exclude=[HasCollision]) 28 for model, pose, scale in zip(qr.model, qr.pose, qr.scale): 29 rl.DrawModel(model.item().model, pose[0:3, 3].tolist(), scale.item(), rl.WHITE) 30 31 qr = world.query(HasModel, HasPose, GroundFloor) 32 for model, pose, scale in zip(qr.model, qr.pose, qr.scale): 33 rl.DrawModel(model.item().model, pose[0:3, 3].tolist(), scale.item(), rl.WHITE) 34 35 qr = world.query(HasModel, HasPose, HasCollision, exclude=[GroundFloor]) 36 collider_size = qr.collider_bbox[:, 1] - qr.collider_bbox[:, 0] 37 for pose, kind, radius, size, colliding in zip(qr.pose, qr.collider_kind, qr.collider_radii, 38 collider_size, qr.is_colliding): 39 color = rl.RED if colliding else rl.GREEN 40 if kind == ColliderKinds.SPHERE: 41 rl.DrawSphere(pose[0:3, 3].tolist(), radius.item(), color) 42 else: # AABB 43 rl.DrawCube(pose[0:3, 3].tolist(), *size.tolist(), color) 44 45 else: 46 qr = world.query(HasModel, HasPose) 47 for i, (model, pose, scale) in enumerate(zip(qr.model, qr.pose, qr.scale)): 48 if camera_entity_id is not None and qr.entity_ids[i] == camera_entity_id: # don't draw yourself in FPV 49 continue 50 rl.DrawModel(model.item().model, pose[0:3, 3].tolist(), scale.item(), rl.WHITE) 51 52 for drawable in (extra_drawables or []): # e.g. plugins 53 drawable.draw(world=world, entity_id=camera_entity_id) 54 for callable_fn in (extra_callables or []): 55 callable_fn(world=world) 56 57 if display_uav_trace: 58 assert uav_traces is not None, uav_traces 59 for traces in uav_traces.values(): 60 for trace in traces.keys(): 61 rl.DrawSphere(trace, 0.05, rl.GREEN) 62 63 rl.EndMode3D() 64 65 if wireframe_mode: 66 rl.rlDisableWireMode() 67 68 if display_hud: 69 n_robots = len(world.query(HasFPV)) 70 msg = f"{camera.label}\nObjects: {len(world)} (robots: {n_robots})" 71 rl.DrawText(msg.encode(), 10, 10, 20, rl.DARKGRAY) 72 rl.DrawFPS(rl.GetScreenWidth() - 80, 10)
Only the main 3D render camera(s). For FPV rendering on a texture, use FPVRenderSystem.
class
PhysicsSystem:
10class PhysicsSystem: 11 """ 12 A orchestration physics system that runs each subtick. It does 4 steps atomically: 13 - candidate motion: move robots based on their physics 'level' (+other forces) 14 - detect collisions and other similar physical events on the candidate new pose 15 - update the candidate position based on the collisions 16 - commit the new physics state 17 """ 18 19 def _motion_from_inputs(self, world: World, dt: float, physics_levels: list[PhysicsLevel] | None = None): 20 qr = world.query(HasMotion) 21 physics_levels = physics_levels or [PhysicsLevel.LEVEL_1, PhysicsLevel.LEVEL_2] 22 23 for level in physics_levels: 24 # TODO: we definitely need a syntax for this pattern (equivalent of: SELECT x where y > 5) 25 mask = (qr.physics_level[:, 0] == level) # (N, ) mask for all the entities (equivalent of where level=level) 26 motion_input = qr.motion_input[mask] 27 if len(motion_input) == 0: 28 continue 29 30 if level == PhysicsLevel.LEVEL_1: 31 vel = motion_level1(motion_input=motion_input, max_velocities=qr.max_velocities[mask]) 32 qr.candidate_velocity[mask] = vel 33 elif level == PhysicsLevel.LEVEL_2: 34 vel, acc = motion_level2(motion_input=motion_input, dt=dt, current_velocity=qr.velocity[mask], 35 max_accelerations=qr.max_accelerations[mask], 36 drag_coefficient=qr.drag_coefficient[mask]) 37 qr.candidate_velocity[mask] = vel 38 qr.candidate_acceleration[mask] = acc 39 else: 40 logger.log_every_s(f"Unknown physics level: {level}", "ERROR", freq_s=2) 41 42 def _integrate_velocity_into_pose(self, world: World, dt: float): 43 qr = world.query(HasMotion, HasPose) 44 qr.candidate_pose = integrate_velocity_into_pose(qr.pose, qr.candidate_velocity, dt) 45 46 def _detect_collisions(self, world: World, cell_size: Point3D): 47 qr = world.query(HasModel, HasCollision, HasPose) 48 49 # Step 1) Update the static collider model using the future pose 50 qr.collider_radii = qr.model_radius * qr.scale 51 qr.collider_bbox = qr.candidate_pose[:, 0:3, 3][:, None] + qr.model_bbox * qr.scale[..., None] 52 53 qr.is_colliding = False # note: this broadcasts over all entities in the query result! 54 entities = [world.get_entity(eid) for eid in qr.entity_ids] 55 grid = make_grid_3d(entities, cell_size) 56 57 for cell_entities in grid.values(): 58 for i, e1 in enumerate(cell_entities): 59 for e2 in cell_entities[i+1:]: 60 if check_collision(e1, e2): 61 e1.is_colliding = e2.is_colliding = True 62 63 def _update_motion_from_collisions(self, world: World): 64 qr = world.query(HasPose, HasCollision, HasMotion) 65 # NOTE: bounce back by negating velocity. We should use some sort of impulse addition + normals 66 # here to figure out the direction of the velocity. 67 qr.candidate_pose = np.where(qr.is_colliding[..., None], qr.pose, qr.candidate_pose) 68 qr.candidate_velocity = np.where(qr.is_colliding, -qr.candidate_velocity, qr.candidate_velocity) 69 70 def _commit(self, world: World): 71 qr = world.query(HasPose, HasMotion) 72 qr.pose = qr.candidate_pose 73 qr.velocity = qr.candidate_velocity 74 qr.acceleration = qr.candidate_acceleration 75 76 def __call__(self, world: World, dt: float, cell_size: Point3D, physics_levels: list[PhysicsLevel] | None = None): 77 self._motion_from_inputs(world, dt, physics_levels) 78 self._integrate_velocity_into_pose(world, dt) 79 self._detect_collisions(world, cell_size) 80 self._update_motion_from_collisions(world) 81 self._commit(world)
A orchestration physics system that runs each subtick. It does 4 steps atomically:
- candidate motion: move robots based on their physics 'level' (+other forces)
- detect collisions and other similar physical events on the candidate new pose
- update the candidate position based on the collisions
- commit the new physics state
class
FPVRenderSystem:
12class FPVRenderSystem: 13 """FPVRenderSystem - renders all the connected eids to custom textures that are streamed to the network clients""" 14 def __call__(self, world: World, connected_eids: list[EntityId], extra_drawables: list[Drawable] | None = None): 15 for eid in connected_eids: 16 entity = world.get_entity(eid) 17 fpv_texture: "rl.RenderTexture" = entity.fpv_texture.item() 18 fpv_data: FPVData = entity.fpv_data.item() 19 camera: Camera = entity.fpv_camera.item() 20 21 rl.BeginTextureMode(fpv_texture) 22 rl.ClearBackground(DEFAULT_BACKGROUND) 23 rl.BeginMode3D(camera.camera[0]) 24 25 qr = world.query(HasModel, HasPose) 26 for i, (model, pose, scale) in enumerate(zip(qr.model, qr.pose, qr.scale)): 27 if qr.entity_ids[i] == eid: # don't draw yourself in FPV 28 continue 29 rl.DrawModel(model.item().model, pose[0:3, 3].tolist(), scale.item(), rl.WHITE) 30 31 for drawable in (extra_drawables or []): # e.g. plugins 32 drawable.draw(world, entity_id=eid) 33 34 rl.EndMode3D() 35 rl.EndTextureMode() 36 37 fpv_img = rl.LoadImageFromTexture(fpv_texture.texture) 38 fpv_img_ptr = rl.ffi.new("Image *", fpv_img) 39 rl.ImageFlipVertical(fpv_img_ptr) 40 with fpv_data.lock: 41 fpv_data.frame = bytes(rl.ffi.buffer(fpv_img_ptr.data, len(fpv_data.frame))) 42 fpv_data.frame_id += 1 43 fpv_data._frame_compressed = None # pylint: disable=protected-access 44 rl.UnloadImage(fpv_img_ptr[0])
FPVRenderSystem - renders all the connected eids to custom textures that are streamed to the network clients