rs_opw_kinematics/visualize/
visualization.rs

1//! Provides visualization window with sliders for angles and pose.
2//!
3//! <img src="https://github.com/user-attachments/assets/3a0e6293-519e-455f-bf8b-0ff1090103b1" alt="screenshot" width="300"/>
4//!
5//! The [`crate::kinematics_with_shape::KinematicsWithShape`] structure fully integrates kinematics
6//! with 3D meshes representing the robot, making it straightforward to visualize.
7//! To display the robot, simply pass this structure to the built-in function
8//! [`visualize_robot`].  
9//!
10//! ```ignore
11//! fn main() {
12//!     use std::ops::RangeInclusive;
13//!     use rs_opw_kinematics::kinematics_with_shape::KinematicsWithShape;
14//!     use rs_opw_kinematics::visualization;
15//!
16//!     // See `complete_visible_robot example` how to build this structure
17//!     let robot: KinematicsWithShape = ...
18//!
19//!     // Define the initial joint angles to display the robot in a specific position
20//!     let initial_angles = [173., -8., -94., 6., 83., 207.];
21//!
22//!     // Define boundaries for XYZ drawbars in the visualization GUI
23//!     let tcp_box: [RangeInclusive<f64>; 3] = [-2.0..=2.0, -2.0..=2.0, 1.0..=2.0];
24//!
25//!     visualization::visualize_robot(robot, initial_angles, tcp_box);
26//! }
27//! ```
28//!
29//! ### Purpose
30//! Visualization serves as a verification tool to ensure that parameters, meshes, tool, and base setup are correct,
31//! rather than as a production feature. Using Bevy, this visualization displays the robot (mounted on its base
32//! and equipped with the tool), various environment objects, and manipulable handles for the robot.
33//!
34//! ### Features
35//! In the visualization window, joint positions can be adjusted for forward kinematics, or the tool center point
36//! can be set using Cartesian coordinates for inverse kinematics.
37//!
38//! - **Collision Detection**: Active in both kinematics modes but functions differently:
39//!   - **Inverse Kinematics**: Movement is restricted to avoid collisions entirely (the robot will not move if a
40//!     collision would occur).
41//!   - **Forward Kinematics**: Collisions are permitted, but colliding robot joints and environment objects are highlighted.
42
43use crate::camera_controller::{camera_controller_system, CameraController};
44use crate::collisions::SafetyDistances;
45use crate::kinematic_traits::{Joints, Kinematics, Pose, ENV_START_IDX, J_BASE, J_TOOL};
46use crate::kinematics_with_shape::KinematicsWithShape;
47use crate::utils;
48use bevy::prelude::*;
49use bevy_egui::{egui, EguiContexts, EguiPlugin};
50use nalgebra::{Isometry3, Translation3, UnitQuaternion, Vector3};
51use parry3d::shape::TriMesh;
52use std::collections::HashSet;
53use std::ops::RangeInclusive;
54use std::time::Instant;
55use bevy::render::render_asset::RenderAssetUsages;
56
57// Convert a parry3d `TriMesh` into a bevy `Mesh` for rendering
58fn trimesh_to_bevy_mesh(trimesh: &TriMesh) -> Mesh {
59    let mut mesh = Mesh::new(bevy::render::mesh::PrimitiveTopology::TriangleList, RenderAssetUsages::RENDER_WORLD);
60
61    // Step 1: Extract vertices and indices from the TriMesh
62    let vertices: Vec<_> = trimesh.vertices().iter().map(|v| [v.x, v.y, v.z]).collect();
63    let indices: Vec<_> = trimesh
64        .indices()
65        .iter()
66        .flat_map(|i| i.to_vec())
67        .collect::<Vec<_>>();
68
69    // Step 2: Initialize normal vectors for each vertex
70    let mut normals: Vec<[f32; 3]> = vec![[0.0, 0.0, 0.0]; vertices.len()];
71
72    // Step 3: Calculate normals for each face (triangle)
73    for triangle in trimesh.indices() {
74        let i0 = triangle[0] as usize;
75        let i1 = triangle[1] as usize;
76        let i2 = triangle[2] as usize;
77
78        // Get the three vertices of the triangle
79        let v0 = nalgebra::Vector3::new(vertices[i0][0], vertices[i0][1], vertices[i0][2]);
80        let v1 = nalgebra::Vector3::new(vertices[i1][0], vertices[i1][1], vertices[i1][2]);
81        let v2 = nalgebra::Vector3::new(vertices[i2][0], vertices[i2][1], vertices[i2][2]);
82
83        // Calculate the two edge vectors
84        let edge1 = v1 - v0;
85        let edge2 = v2 - v0;
86
87        // Calculate the normal using the cross product of the two edges
88        let normal = edge1.cross(&edge2).normalize();
89
90        for &i in &[i0, i1, i2] {
91            normals[i][0] += normal.x;
92            normals[i][1] += normal.y;
93            normals[i][2] += normal.z;
94        }
95    }
96
97    // Step 4: Normalize all vertex normals
98    for normal in normals.iter_mut() {
99        let length = (normal[0] * normal[0] + normal[1] * normal[1] + normal[2] * normal[2]).sqrt();
100        if length > 0.0 {
101            normal[0] /= length;
102            normal[1] /= length;
103            normal[2] /= length;
104        }
105    }
106
107    // Step 5: Insert attributes into the mesh
108    mesh.insert_attribute(Mesh::ATTRIBUTE_POSITION, vertices);
109    mesh.insert_attribute(Mesh::ATTRIBUTE_NORMAL, normals); // Add the flipped normals
110
111    // Step 6: Set the indices
112    mesh.insert_indices(bevy::render::mesh::Indices::U32(indices));
113
114    mesh
115}
116
117/// Data to store the current joint angles and TCP as they are shown in control panel
118#[derive(Resource)]
119struct RobotControls {
120    joint_angles: [f32; 6],
121    tcp: [f64; 6],
122    tcp_box: [RangeInclusive<f64>; 3], // Boundaries for sliders in GUI to move the tool center point arround
123    initial_joint_angles: [f32; 6],    // Initial angles at the start of visualization
124    previous_joint_angles: [f32; 6],   // Store previous joint angles here
125    previous_tcp: [f64; 6],
126    safety_distance: f32,
127    previous_safety_distance: f32
128}
129
130impl RobotControls {
131    fn set_tcp_from_pose(&mut self, pose: &Isometry3<f64>) {
132        let (roll, pitch, yaw) = pose.rotation.to_rotation_matrix().euler_angles();
133        self.tcp = [
134            pose.translation.x,
135            pose.translation.y,
136            pose.translation.z,
137            roll.to_degrees(),
138            pitch.to_degrees(),
139            yaw.to_degrees(),
140        ];
141    }
142
143    fn pose(&self) -> Pose {
144        fn quat_from_euler(this: &RobotControls) -> UnitQuaternion<f64> {
145            let roll = this.tcp[3].to_radians();
146            let pitch = this.tcp[4].to_radians();
147            let yaw = this.tcp[5].to_radians();
148
149            // Combine rotations in roll-pitch-yaw order
150            UnitQuaternion::from_axis_angle(&Vector3::z_axis(), roll)
151                * UnitQuaternion::from_axis_angle(&Vector3::y_axis(), pitch)
152                * UnitQuaternion::from_axis_angle(&Vector3::x_axis(), yaw)
153        }
154
155        Isometry3::from_parts(
156            Translation3::new(self.tcp[0], self.tcp[1], self.tcp[2]),
157            quat_from_euler(&self),
158        )
159    }
160}
161
162// Resource to store the current robot instance
163#[derive(Resource)]
164struct Robot {
165    kinematics: KinematicsWithShape,
166    safety: SafetyDistances,
167    joint_meshes: Option<[Handle<Mesh>; 6]>, // Precomputed and converted meshes
168    material: Option<Handle<StandardMaterial>>,
169    tool_material: Option<Handle<StandardMaterial>>,
170    base_material: Option<Handle<StandardMaterial>>,
171    environment_material: Option<Handle<StandardMaterial>>,
172    colliding_material: Option<Handle<StandardMaterial>>, // Highlight colliding components in color
173    tool: Option<Handle<Mesh>>,
174    base: Option<Handle<Mesh>>,
175    environment: Vec<Handle<Mesh>>, // Environment objects
176}
177
178/// Visualize the given robot, starting from the given initial angles (given in degrees)
179/// The sliders for specifying the tool center point location with take the boundaries
180/// from the tcp_box (given in meters). Bevy will be used for visualization.
181pub fn visualize_robot(
182    robot: KinematicsWithShape,
183    intial_angles: [f32; 6],
184    tcp_box: [RangeInclusive<f64>; 3],
185) {
186    let safety = robot.body.safety.clone();
187    visualize_robot_with_safety(
188        robot,
189        intial_angles,
190        tcp_box,
191        &safety
192    )
193}
194
195pub fn visualize_robot_with_safety(
196    robot: KinematicsWithShape,
197    intial_angles: [f32; 6],
198    tcp_box: [RangeInclusive<f64>; 3],
199    safety_distances: &SafetyDistances,
200) {
201    App::new()
202        .add_plugins((DefaultPlugins, EguiPlugin)) // Use add_plugin for Egui
203        .insert_resource(RobotControls {
204            initial_joint_angles: intial_angles.clone(),
205            joint_angles: intial_angles.clone(),
206            tcp: [0.0; 6],
207            tcp_box: tcp_box,
208            previous_joint_angles: intial_angles.clone(),
209            previous_tcp: [0.0; 6],
210            safety_distance: 0.05,
211            previous_safety_distance: 0.0
212        })
213        .insert_resource(Robot {
214            kinematics: robot,
215            safety: safety_distances.clone(),
216            joint_meshes: None,
217            tool: None,
218            base: None,
219            environment: Vec::new(),
220            material: None,
221            tool_material: None,
222            base_material: None,
223            environment_material: None,
224            colliding_material: None,
225        })
226        .add_systems(Startup, setup) // Register setup system in Startup phase
227        .add_systems(
228            Update,
229            (update_robot, camera_controller_system, control_panel),
230        ) // Add systems without .system()
231        .run();
232}
233
234fn setup(
235    mut commands: Commands,
236    mut meshes: ResMut<Assets<Mesh>>,
237    mut materials: ResMut<Assets<StandardMaterial>>,
238    mut robot_controls: ResMut<RobotControls>,
239    mut robot: ResMut<Robot>,
240) {
241    fn mesh_for_joint(
242        meshes: &mut ResMut<Assets<Mesh>>,
243        robot: &ResMut<Robot>,
244        k: usize,
245    ) -> Handle<Mesh> {
246        meshes.add(trimesh_to_bevy_mesh(&robot.kinematics.body.joint_meshes[k]))
247    }
248
249    // Precompute the mesh for each of the six robot joints
250    robot.joint_meshes = Some([
251        mesh_for_joint(&mut meshes, &robot, 0),
252        mesh_for_joint(&mut meshes, &robot, 1),
253        mesh_for_joint(&mut meshes, &robot, 2),
254        mesh_for_joint(&mut meshes, &robot, 3),
255        mesh_for_joint(&mut meshes, &robot, 4),
256        mesh_for_joint(&mut meshes, &robot, 5),
257    ]);
258
259    robot.environment = robot
260        .kinematics
261        .body
262        .collision_environment
263        .iter()
264        .map(|x| meshes.add(trimesh_to_bevy_mesh(&x.mesh)))
265        .collect();
266
267    if let Some(tool) = robot.kinematics.body.tool.as_ref() {
268        robot.tool = Some(meshes.add(trimesh_to_bevy_mesh(&tool)));
269        robot.tool_material = Some(materials.add(StandardMaterial {
270            base_color: Color::srgb(0.8, 1.0, 0.8),
271            metallic: 0.7,
272            perceptual_roughness: 0.05,
273            ..default()
274        }));
275    }
276
277    if let Some(base) = robot.kinematics.body.base.as_ref() {
278        robot.base = Some(meshes.add(trimesh_to_bevy_mesh(&base.mesh)));
279        robot.base_material = Some(materials.add(StandardMaterial {
280            base_color: Color::srgb(0.8, 1.0, 0.8),
281            metallic: 0.7,
282            perceptual_roughness: 0.05,
283            ..default()
284        }));
285    }
286
287    robot.material = Some(materials.add(StandardMaterial {
288        base_color: Color::srgb(1.0, 1.0, 0.0),
289        metallic: 0.7,
290        perceptual_roughness: 0.1,
291        ..default()
292    }));
293
294    robot.environment_material = Some(materials.add(StandardMaterial {
295        base_color: Color::srgb(0.5, 0.5, 0.5),
296        metallic: 1.0,
297        perceptual_roughness: 1.0,
298        ..default()
299    }));
300
301    robot.colliding_material = Some(materials.add(StandardMaterial {
302        base_color: Color::srgb(1.0, 0.1, 0.1),
303        metallic: 1.0,
304        perceptual_roughness: 0.1,
305        ..default()
306    }));
307
308    // Visualize the robot joints with the initial joint values
309    let angles = utils::joints(&robot_controls.initial_joint_angles);
310    visualize_robot_joints(&mut commands, & mut robot, &angles, robot_controls.safety_distance);
311    let cartesian = robot.kinematics.kinematics.forward(&angles);
312    robot_controls.set_tcp_from_pose(&cartesian);
313
314    // Add camera and light
315    commands.spawn(DirectionalLightBundle {
316        directional_light: DirectionalLight {
317            illuminance: 30000.0,
318            ..default()
319        },
320        transform: Transform::from_xyz(5.0, 8.0, 5.0).looking_at(Vec3::ZERO, Vec3::Y),
321        ..default()
322    });
323
324    commands.spawn(DirectionalLightBundle {
325        directional_light: DirectionalLight {
326            illuminance: 30000.0,
327            ..default()
328        },
329        transform: Transform::from_xyz(-5.0, 0.0, -5.0).looking_at(Vec3::ZERO, Vec3::Y),
330        ..default()
331    });
332
333    commands.spawn((
334        Camera3dBundle {
335            transform: Transform {
336                translation: Vec3::new(0.0, 5.0, 20.0), // Adjust camera position as needed
337                // Apply a 90-degree rotation around the X-axis
338                rotation: Quat::from_rotation_x(std::f32::consts::FRAC_PI_2),
339                ..default()
340            },
341            ..default()
342        },
343        CameraController::default(), // Custom component for controlling the camera
344    ));
345}
346
347/// This function generates a visual representation of each joint of a robot
348/// based on the provided joint angles, rendering each joint shape at its
349/// calculated position and orientation using Bevy's `PbrBundle`.
350///
351/// # Parameters:
352/// - `commands`: Mutable reference to Bevy's `Commands`, used to issue entity spawn commands.
353/// - `robot`: Reference to the robot's kinematic model and shapes (`KinematicsWithShape`).
354/// - `angles`: Joint angles used to compute forward kinematics for rendering.
355///
356fn visualize_robot_joints(commands: &mut Commands, robot: &mut ResMut<Robot>, angles: &Joints, safety_distance: f32) {
357    // Helper function to transform coordinates to Bevy's coordinate system
358    fn as_bevy(transform: &Isometry3<f32>) -> (Vec3, Quat) {
359        let translation = transform.translation.vector;
360        let rotation = transform.rotation;
361        (
362            Vec3::new(translation.x, translation.y, translation.z),
363            Quat::from_xyzw(rotation.i, rotation.j, rotation.k, rotation.w),
364        )
365    }
366
367    /// Spawns a `PbrBundle` entity for a joint, with specified mesh, material, translation, and rotation.
368    fn spawn_joint(
369        commands: &mut Commands,
370        mesh: &Handle<Mesh>,
371        material: Handle<StandardMaterial>,
372        pose: &Isometry3<f32>,
373    ) {
374        let (translation, rotation) = as_bevy(&pose);
375        commands.spawn(PbrBundle {
376            mesh: mesh.clone(),
377            material,
378            transform: Transform {
379                translation,
380                rotation,
381                ..default()
382            },
383            ..default()
384        });
385    }
386
387    // Detect collisions between joints
388    let start = Instant::now();
389    robot.safety.to_environment = safety_distance;
390    robot.safety.to_robot_default = safety_distance;
391    let collisions = robot.kinematics.near(&angles, &robot.safety);
392    println!("Time for collision check: {:?}", start.elapsed());
393
394    let colliding_segments: HashSet<_> = collisions.iter().flat_map(|(x, y)| [*x, *y]).collect();
395
396    // Visualize each joint in the robot
397    let positioned_robot = robot.kinematics.positioned_robot(angles);
398    for (j, positioned_joint) in positioned_robot.joints.iter().enumerate() {
399        spawn_joint(
400            commands,
401            &robot.joint_meshes.as_ref().unwrap()[j],
402            maybe_colliding_material(&robot, &robot.material, &colliding_segments, &j),
403            &positioned_joint.transform,
404        );
405    }
406
407    // Visualize the tool if present
408    if let (Some(tool), Some(tool_joint)) = (&robot.tool, positioned_robot.tool.as_ref()) {
409        spawn_joint(
410            commands,
411            tool,
412            maybe_colliding_material(&robot, &robot.tool_material, &colliding_segments, &J_TOOL),
413            &tool_joint.transform,
414        );
415    }
416
417    // Visualize the base if present
418    if let (Some(base), Some(base_joint)) = (&robot.base, robot.kinematics.body.base.as_ref()) {
419        spawn_joint(
420            commands,
421            base,
422            maybe_colliding_material(&robot, &robot.base_material, &colliding_segments, &J_BASE),
423            &base_joint.base_pose,
424        );
425    }
426
427    // Add environment objects
428    for (i, environment_joint) in positioned_robot.environment.iter().enumerate() {
429        spawn_joint(
430            commands,
431            &robot.environment[i],
432            maybe_colliding_material(
433                &robot,
434                &robot.environment_material,
435                &colliding_segments,
436                &(ENV_START_IDX + i),
437            ),
438            &environment_joint.pose,
439        );
440    }
441}
442
443/// Returns a colliding material if the joint is in `colliding_segments`, otherwise returns the default material.
444fn maybe_colliding_material(
445    robot: &ResMut<Robot>,
446    material: &Option<Handle<StandardMaterial>>,
447    colliding_segments: &HashSet<usize>,
448    joint: &usize,
449) -> Handle<StandardMaterial> {
450    let selected_material = if colliding_segments.contains(joint) {
451        &robot.colliding_material
452    } else {
453        material
454    };
455    selected_material.as_ref().unwrap().clone()
456}
457
458// Update the robot when joint angles change
459fn update_robot(
460    mut commands: Commands,
461    mut controls: ResMut<RobotControls>,
462    mut robot: ResMut<Robot>,
463    query: Query<Entity, With<Handle<Mesh>>>, // Query entities with Mesh components
464) {
465    if controls.safety_distance != controls.previous_safety_distance {
466        for entity in query.iter() {
467            commands.entity(entity).despawn();
468        }
469        let angles = utils::joints(&controls.joint_angles);
470        visualize_robot_joints(&mut commands, &mut robot, &angles, controls.safety_distance);
471        controls.previous_safety_distance = controls.safety_distance;
472    } else if controls.joint_angles != controls.previous_joint_angles {
473        // Despawn the existing visualized robot joints
474        for entity in query.iter() {
475            commands.entity(entity).despawn();
476        }
477
478        // Revisualize the robot joints with the updated joint angles
479        // Visualize each joint of the robot
480        let angles = utils::joints(&controls.joint_angles);
481        visualize_robot_joints(&mut commands, &mut robot, &angles, controls.safety_distance);
482        controls.previous_joint_angles = controls.joint_angles.clone();
483
484        // Update the TCP position (this is the end of the tool, not the base)
485        let pose = robot.kinematics.kinematics.forward(&angles);
486        controls.set_tcp_from_pose(&pose);
487        controls.previous_tcp = controls.tcp;
488        controls.previous_safety_distance = controls.safety_distance;        
489    } else if controls.tcp != controls.previous_tcp {
490        // Revisualize the robot joints with the updated joint angles
491        let angles = utils::joints(&controls.joint_angles);
492        let pose = controls.pose();
493
494        // We ask kinematics with shape to do the inverse kinematics.
495        // This means, colliding solutions will be discarded.
496        let start = Instant::now();
497        let ik = robot.kinematics.inverse_continuing(&pose, &angles);
498        println!("Time for inverse kinematics: {:?}", start.elapsed());
499        if ik.len() > 0 {
500            for entity in query.iter() {
501                commands.entity(entity).despawn();
502            }
503            let angles = ik[0];
504            visualize_robot_joints(&mut commands, &mut robot, &angles, controls.safety_distance);
505
506            // Update joint angles to match the current position
507            controls.joint_angles = utils::to_degrees(&angles);
508        } else {
509            println!(
510                "  no solution for pose {:.1} {:.1} {:.1} rotation {:.1} {:.1} {:.1}",
511                controls.tcp[0],
512                controls.tcp[1],
513                controls.tcp[2],
514                controls.tcp[3],
515                controls.tcp[4],
516                controls.tcp[5]
517            );
518        }
519        controls.previous_tcp = controls.tcp.clone();
520        controls.previous_joint_angles = controls.joint_angles;
521        controls.previous_safety_distance = controls.safety_distance;
522    }
523}
524
525// Control panel for adjusting joint angles and tool center point
526fn control_panel(mut egui_contexts: EguiContexts, mut controls: ResMut<RobotControls>) {
527    bevy_egui::egui::Window::new("Robot Controls").show(egui_contexts.ctx_mut(), |ui| {
528        ui.label("Joint rotations");
529        for (i, angle) in controls.joint_angles.iter_mut().enumerate() {
530            ui.add(egui::Slider::new(angle, -225.0..=225.0).text(format!("Joint {}", i + 1)));
531        }
532
533        let tcp_x_range = controls.tcp_box[0].clone();
534        let tcp_y_range = controls.tcp_box[1].clone();
535        let tcp_z_range = controls.tcp_box[2].clone();
536
537        ui.add_space(10.0);
538        ui.label("Tool center point (TCP)");
539        ui.add(egui::Slider::new(&mut controls.tcp[0], tcp_x_range).text("X"));
540        ui.add(egui::Slider::new(&mut controls.tcp[1], tcp_y_range).text("Y"));
541        ui.add(egui::Slider::new(&mut controls.tcp[2], tcp_z_range).text("Z"));
542
543        ui.add_space(10.0);
544        ui.label("TCP Euler angles");
545        ui.add(egui::Slider::new(&mut controls.tcp[3], -90.0..=90.0).text("Roll"));
546        ui.add(egui::Slider::new(&mut controls.tcp[4], -90.0..=90.0).text("Pitch"));
547        ui.add(egui::Slider::new(&mut controls.tcp[5], -90.0..=90.0).text("Yaw"));
548
549        ui.add_space(10.0);
550        ui.label("Safety distance");
551        ui.add(egui::Slider::new(&mut controls.safety_distance, 0.0..=0.5).text("Max"));
552    });
553}