extends MeshInstance3D ## Displays JPEG webcam frames received from the robot on a quad mesh. ## The quad is positioned in front of the user's view (child of XRCamera3D). ## ## Receives JPEG bytes via the webcam_frame_received signal from TeleopClient. ## Display settings @export var default_color := Color(0.1, 0.1, 0.1, 0.8) ## State var _texture: ImageTexture var _material: StandardMaterial3D var _frame_count: int = 0 var _last_frame_time: int = 0 var _fps: float = 0.0 var _fps_update_timer: float = 0.0 var _has_received_frame: bool = false func _ready() -> void: # Always create a fresh material — instanced scenes share sub_resources, # so we must not reuse the scene's material or all quads show the same texture _material = StandardMaterial3D.new() _material.shading_mode = BaseMaterial3D.SHADING_MODE_UNSHADED _material.albedo_color = default_color _material.transparency = BaseMaterial3D.TRANSPARENCY_ALPHA material_override = _material # Create initial texture (will be replaced on first frame) _texture = ImageTexture.new() print("[WebcamDisplay] %s ready, waiting for frames..." % name) func _process(delta: float) -> void: # Update FPS counter _fps_update_timer += delta if _fps_update_timer >= 1.0: _fps = _frame_count / _fps_update_timer _frame_count = 0 _fps_update_timer = 0.0 ## Called when a JPEG webcam frame is received from the server. ## Connected via signal from TeleopClient in Main.gd. func _on_webcam_frame(jpeg_bytes: PackedByteArray) -> void: if jpeg_bytes.size() < 2: return var image := Image.new() var err := image.load_jpg_from_buffer(jpeg_bytes) if err != OK: if _frame_count == 0: printerr("[WebcamDisplay] Failed to decode JPEG frame (size=%d, err=%d)" % [jpeg_bytes.size(), err]) return # Update texture from decoded image if _texture.get_image() == null or _texture.get_width() != image.get_width() or _texture.get_height() != image.get_height(): _texture = ImageTexture.create_from_image(image) _material.albedo_texture = _texture _material.albedo_color = Color.WHITE # Full brightness once we have a real image print("[WebcamDisplay] Texture created: %dx%d" % [image.get_width(), image.get_height()]) else: _texture.update(image) _frame_count += 1 _last_frame_time = Time.get_ticks_msec() if not _has_received_frame: _has_received_frame = true print("[WebcamDisplay] First frame received!") ## Get display FPS func get_fps() -> float: return _fps ## Check if frames are being received func is_receiving() -> bool: if not _has_received_frame: return false # Consider stale if no frame for 2 seconds return (Time.get_ticks_msec() - _last_frame_time) < 2000