forked from pz4kybsvg/Conception
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
880 lines
36 KiB
880 lines
36 KiB
#include "drake/geometry/drake_visualizer.h"
|
|
|
|
#include <algorithm>
|
|
#include <array>
|
|
#include <map>
|
|
#include <set>
|
|
#include <utility>
|
|
#include <vector>
|
|
|
|
#include "drake/common/default_scalars.h"
|
|
#include "drake/common/extract_double.h"
|
|
#include "drake/common/value.h"
|
|
#include "drake/geometry/proximity/sorted_triplet.h"
|
|
#include "drake/geometry/proximity/volume_to_surface_mesh.h"
|
|
#include "drake/geometry/query_object.h"
|
|
#include "drake/geometry/scene_graph.h"
|
|
#include "drake/geometry/shape_specification.h"
|
|
#include "drake/geometry/utilities.h"
|
|
#include "drake/lcm/drake_lcm.h"
|
|
#include "drake/lcmt_viewer_draw.hpp"
|
|
#include "drake/lcmt_viewer_geometry_data.hpp"
|
|
#include "drake/lcmt_viewer_load_robot.hpp"
|
|
#include "drake/math/rigid_transform.h"
|
|
#include "drake/math/rotation_matrix.h"
|
|
|
|
namespace drake {
|
|
namespace geometry {
|
|
|
|
using Eigen::Quaterniond;
|
|
using internal::MakeLcmChannelNameForRole;
|
|
using internal::SortedTriplet;
|
|
using math::RigidTransformd;
|
|
using std::array;
|
|
using std::make_unique;
|
|
using std::map;
|
|
using std::move;
|
|
using std::set;
|
|
using std::vector;
|
|
using systems::Context;
|
|
using systems::EventStatus;
|
|
using systems::SystemTypeTag;
|
|
|
|
namespace {
|
|
|
|
/* Create an lcm message for a hydroelastic mesh representation of the geometry
|
|
indicated by `geometry_id`.
|
|
|
|
The geometry data message is marked as a MESH, but rather than having a
|
|
path to a parseable file stored in it, the actual mesh data is stored.
|
|
|
|
This function shares implementation details with the ShapeToLcm reifier (in
|
|
terms of handling pose and color). Ultimately, when the Mesh shape
|
|
specification supports in-memory mesh definitions, this can be rolled into
|
|
ShapeToLcm and it will more fully share that class's code for handling pose
|
|
and color.
|
|
|
|
@param geometry_id The id of the geometry to draw.
|
|
@param inspector The SceneGraphInspector from which the mesh will be drawn.
|
|
@param X_PG The pose of the geometry in the parent frame.
|
|
@param in_color The color to apply to the mesh.
|
|
@pre The geometry actually has a hydroelastic mesh. */
|
|
template <typename T>
|
|
lcmt_viewer_geometry_data MakeHydroMesh(GeometryId geometry_id,
|
|
const SceneGraphInspector<T>& inspector,
|
|
const RigidTransformd& X_PG,
|
|
const Rgba& in_color) {
|
|
auto hydro_mesh = inspector.maybe_get_hydroelastic_mesh(geometry_id);
|
|
DRAKE_DEMAND(!std::holds_alternative<std::monostate>(hydro_mesh));
|
|
// SceneGraphInspector guarantees whichever pointer is "held" in the variant
|
|
// is non-null.
|
|
const TriangleSurfaceMesh<double>& surface_mesh =
|
|
std::holds_alternative<const TriangleSurfaceMesh<double>*>(hydro_mesh)
|
|
? *std::get<const TriangleSurfaceMesh<double>*>(hydro_mesh)
|
|
: ConvertVolumeToSurfaceMesh(
|
|
*std::get<const VolumeMesh<double>*>(hydro_mesh));
|
|
|
|
lcmt_viewer_geometry_data geometry_data;
|
|
|
|
// Saves the location and orientation of the visualization geometry in the
|
|
// `lcmt_viewer_geometry_data` object. The location and orientation are
|
|
// specified in the body's frame.
|
|
Eigen::Map<Eigen::Vector3f> position(geometry_data.position);
|
|
position = X_PG.translation().template cast<float>();
|
|
// LCM quaternion must be w, x, y, z.
|
|
Quaterniond q(X_PG.rotation().ToQuaternion());
|
|
geometry_data.quaternion[0] = q.w();
|
|
geometry_data.quaternion[1] = q.x();
|
|
geometry_data.quaternion[2] = q.y();
|
|
geometry_data.quaternion[3] = q.z();
|
|
|
|
Eigen::Map<Eigen::Vector4f> color(geometry_data.color);
|
|
Eigen::Vector4d color_vec(in_color.r(), in_color.g(), in_color.b(),
|
|
in_color.a());
|
|
color = color_vec.cast<float>();
|
|
|
|
// There are *two* ways to use the MESH geometry type. One is to set the
|
|
// string value with a path to a parseable mesh file (see
|
|
// ImplementGeometry(Mesh) below). The other is to leave the string empty and
|
|
// define the mesh in the float data as:
|
|
// V | T | v0 | v1 | ... vN | t0 | t1 | ... | tM
|
|
// where
|
|
// V: The number of vertices.
|
|
// T: The number of triangles.
|
|
// N: 3V, the number of floating point values for the V vertices.
|
|
// M: 3T, the number of vertex indices for the T triangles.
|
|
geometry_data.type = geometry_data.MESH;
|
|
|
|
// We want a *faceted* mesh. We can achieve this by duplicating the vertices
|
|
// so DrakeVisualizer can't smooth over vertices. So, that means for T
|
|
// triangles we have 3T vertices. Experimentation suggests this redundancy
|
|
// is the simplest way to get a faceted mesh.
|
|
const int num_tris = surface_mesh.num_triangles();
|
|
const int num_verts = 3 * num_tris;
|
|
const int header_floats = 2;
|
|
geometry_data.num_float_data = header_floats + 3 * num_tris + 3 * num_verts;
|
|
geometry_data.float_data.resize(geometry_data.num_float_data);
|
|
auto& float_data = geometry_data.float_data;
|
|
|
|
float_data[0] = num_verts;
|
|
float_data[1] = num_tris;
|
|
|
|
// We simply want to pre-increment before using this index; so we'll decrement
|
|
// it to be just before where we want to write.
|
|
int v_index = header_floats - 1;
|
|
int t_index = v_index + 3 * num_verts;
|
|
|
|
// The index of the most recently added vertex (whose position measures are
|
|
// written at v_index, v_index + 1, and v_index + 2 in float_data).
|
|
int newest_vertex_index = -1;
|
|
for (int f = 0; f < surface_mesh.num_triangles(); ++f) {
|
|
const auto& face = surface_mesh.element(f);
|
|
for (int fv = 0; fv < 3; ++fv) {
|
|
const int v_i = face.vertex(fv);
|
|
const Vector3<float> p_MV =
|
|
surface_mesh.vertex(v_i).template cast<float>();
|
|
float_data[++v_index] = p_MV.x();
|
|
float_data[++v_index] = p_MV.y();
|
|
float_data[++v_index] = p_MV.z();
|
|
float_data[++t_index] = ++newest_vertex_index;
|
|
}
|
|
}
|
|
|
|
// v_index and t_index end with the index of the last element added, so one
|
|
// less than the expected number, hence the "+ 1". So, the vertex index should
|
|
// have walked up to the starting index for triangles and the triangle index
|
|
// should have walked up to the total number of floats.
|
|
DRAKE_DEMAND(header_floats + 3 * num_verts == (v_index + 1));
|
|
DRAKE_DEMAND(geometry_data.num_float_data == (t_index + 1));
|
|
return geometry_data;
|
|
}
|
|
|
|
/* Create an lcm message for a deformable mesh representation of the geometry
|
|
described by `deformable_data`.
|
|
|
|
The geometry data message is marked as a MESH, but rather than having a
|
|
path to a parseable file stored in it, the actual mesh data is stored.
|
|
The string data is used to store the name of the geometry and the pose of the
|
|
geometry is set to identity because the vertex positions are expressed in the
|
|
world frame.
|
|
|
|
When the Mesh shape specification supports in-memory mesh definitions, this
|
|
can be rolled into ShapeToLcm and it will more fully share that class's code
|
|
for handling color. */
|
|
template <typename T>
|
|
lcmt_viewer_geometry_data MakeDeformableSurfaceMesh(
|
|
const VectorX<T>& volume_vertex_positions,
|
|
const internal::DeformableMeshData& deformable_data, const Rgba& in_color) {
|
|
lcmt_viewer_geometry_data geometry_data;
|
|
// The pose is unused and set to identity.
|
|
geometry_data.quaternion[0] = 1;
|
|
geometry_data.quaternion[1] = 0;
|
|
geometry_data.quaternion[2] = 0;
|
|
geometry_data.quaternion[3] = 0;
|
|
geometry_data.position[0] = 0;
|
|
geometry_data.position[1] = 0;
|
|
geometry_data.position[2] = 0;
|
|
|
|
geometry_data.string_data = deformable_data.name;
|
|
|
|
Eigen::Map<Eigen::Vector4f> color(geometry_data.color);
|
|
Eigen::Vector4d color_vec(in_color.r(), in_color.g(), in_color.b(),
|
|
in_color.a());
|
|
color = color_vec.cast<float>();
|
|
|
|
// We can define the mesh in the float data as:
|
|
// V | T | v0 | v1 | ... vN | t0 | t1 | ... | tM
|
|
// where
|
|
// V: The number of vertices.
|
|
// T: The number of triangles.
|
|
// N: 3V, the number of floating point values for the V vertices.
|
|
// M: 3T, the number of vertex indices for the T triangles.
|
|
geometry_data.type = geometry_data.MESH;
|
|
|
|
const int num_tris = deformable_data.surface_triangles.size();
|
|
const int num_verts = deformable_data.surface_to_volume_vertices.size();
|
|
const int header_floats = 2;
|
|
geometry_data.num_float_data = header_floats + 3 * num_tris + 3 * num_verts;
|
|
geometry_data.float_data.resize(geometry_data.num_float_data);
|
|
std::vector<float>& float_data = geometry_data.float_data;
|
|
|
|
float_data[0] = num_verts;
|
|
float_data[1] = num_tris;
|
|
|
|
const int v_start_index = header_floats;
|
|
const int t_start_index = v_start_index + 3 * num_verts;
|
|
// We simply want to pre-increment before using this index; so we'll decrement
|
|
// it to be just before where we want to write.
|
|
int v_index = v_start_index - 1;
|
|
int t_index = t_start_index - 1;
|
|
|
|
for (int f = 0; f < num_tris; ++f) {
|
|
const Vector3<int>& face = deformable_data.surface_triangles[f];
|
|
for (int fv = 0; fv < 3; ++fv) {
|
|
float_data[++t_index] = face[fv];
|
|
}
|
|
}
|
|
for (int i = 0; i < num_verts; ++i) {
|
|
// v_i is the index into the volume mesh
|
|
const int v_i = deformable_data.surface_to_volume_vertices[i];
|
|
for (int d = 0; d < 3; ++d) {
|
|
float_data[++v_index] =
|
|
ExtractDoubleOrThrow(volume_vertex_positions[3 * v_i + d]);
|
|
}
|
|
}
|
|
// v_index and t_index end with the index of the last element added, so one
|
|
// less than the expected number, hence the "+ 1". So, the vertex index should
|
|
// have walked up to the starting index for triangles and the triangle index
|
|
// should have walked up to the total number of floats.
|
|
DRAKE_DEMAND(header_floats + 3 * num_verts == (v_index + 1));
|
|
DRAKE_DEMAND(geometry_data.num_float_data == (t_index + 1));
|
|
return geometry_data;
|
|
}
|
|
|
|
/* Analyzes the tetrahedral mesh topology of the deformble geometry with `g_id`
|
|
to do the following:
|
|
1. Build a surface mesh from each volume mesh.
|
|
2. Create a mapping from surface vertex to volume vertex for each mesh.
|
|
3. Record the expected number of vertices referenced by each tet mesh.
|
|
Store the results in DeformableMeshData.
|
|
@pre g_id corresponds to a deformable geometry. */
|
|
template <typename T>
|
|
internal::DeformableMeshData MakeDeformableMeshData(
|
|
GeometryId g_id, const SceneGraphInspector<T>& inspector) {
|
|
/* For each tet mesh, extract all the border triangles. Those are the
|
|
triangles that are only referenced by a single tet. So, for every tet, we
|
|
examine its four constituent triangle and determine if any other tet
|
|
shares it. Any triangle that is only referenced once is a border triangle.
|
|
Each triangle has a unique key: a SortedTriplet (so the ordering of the
|
|
triangle vertex indices won't matter). The first time we see a triangle, we
|
|
add it to a map. The second time we see the triangle, we remove it. When
|
|
we're done, the keys in the map will be those triangles referenced only once.
|
|
The values in the map represent the triangle, with the vertex indices
|
|
ordered so that they point *out* of the tetrahedron. Therefore,
|
|
they will also point outside of the mesh. A typical tetrahedral element
|
|
looks like:
|
|
|
|
p2 *
|
|
|
|
|
|
|
|
p3 *---* p0
|
|
/
|
|
/
|
|
p1 *
|
|
|
|
The index order for a particular tetrahedron has the order [p0, p1, p2,
|
|
p3]. These local indices enumerate each of the tet triangles with
|
|
outward-pointing normals with respect to the right-hand rule. */
|
|
const array<array<int, 3>, 4> local_indices{
|
|
{{{1, 0, 2}}, {{3, 0, 1}}, {{3, 1, 2}}, {{2, 0, 3}}}};
|
|
|
|
const VolumeMesh<double>* mesh = inspector.GetReferenceMesh(g_id);
|
|
DRAKE_DEMAND(mesh != nullptr);
|
|
|
|
std::map<SortedTriplet<int>, array<int, 3>> border_triangles;
|
|
for (const VolumeElement& tet : mesh->tetrahedra()) {
|
|
for (const array<int, 3>& tet_triangle : local_indices) {
|
|
const array<int, 3> tri{tet.vertex(tet_triangle[0]),
|
|
tet.vertex(tet_triangle[1]),
|
|
tet.vertex(tet_triangle[2])};
|
|
const SortedTriplet triangle_key(tri[0], tri[1], tri[2]);
|
|
// Here we rely on the fact that at most two tets would share a common
|
|
// triangle.
|
|
if (auto itr = border_triangles.find(triangle_key);
|
|
itr != border_triangles.end()) {
|
|
border_triangles.erase(itr);
|
|
} else {
|
|
border_triangles[triangle_key] = tri;
|
|
}
|
|
}
|
|
}
|
|
/* Record the expected minimum number of vertex positions to be received.
|
|
For simplicity we choose a generous upper bound: the total number of
|
|
vertices in the tetrahedral mesh, even though we really only need the
|
|
positions of the vertices on the surface. */
|
|
// TODO(xuchenhan-tri) It might be worthwhile to make largest_index the
|
|
// largest index that lies on the surface. Then, when we create our meshes,
|
|
// if we intentionally construct them so that the surface vertices come
|
|
// first, we will process a very compact representation.
|
|
const int volume_vertex_count = mesh->num_vertices();
|
|
|
|
/* Using a set because the vertices will be nicely ordered. Ideally, we'll
|
|
be extracting a subset of the vertex positions from the input port. We
|
|
optimize cache coherency if we march in a monotonically increasing pattern.
|
|
So, we'll map triangle vertex indices to volume vertex indices in a
|
|
strictly monotonically increasing relationship. */
|
|
set<int> unique_vertices;
|
|
for (const auto& [triangle_key, triangle] : border_triangles) {
|
|
unused(triangle_key);
|
|
for (int j = 0; j < 3; ++j) unique_vertices.insert(triangle[j]);
|
|
}
|
|
|
|
/* This is the *second* documented responsibility of this function: Populate
|
|
the mapping from surface to volume so that we can efficiently extract the
|
|
*surface* vertex positions from the *volume* vertex input. */
|
|
vector<int> surface_to_volume_vertices;
|
|
surface_to_volume_vertices.insert(surface_to_volume_vertices.begin(),
|
|
unique_vertices.begin(),
|
|
unique_vertices.end());
|
|
|
|
/* The border triangles all include indices into the volume vertices. To turn
|
|
them into surface triangles, they need to include indices into the surface
|
|
vertices. Create the volume index --> surface map to facilitate the
|
|
transformation. */
|
|
const int surface_vertex_count =
|
|
static_cast<int>(surface_to_volume_vertices.size());
|
|
map<int, int> volume_to_surface;
|
|
for (int j = 0; j < surface_vertex_count; ++j) {
|
|
volume_to_surface[surface_to_volume_vertices[j]] = j;
|
|
}
|
|
|
|
/* This is the *first* documented responsibility: Create the topology of the
|
|
surface triangle mesh for each volume mesh. Each triangle consists of three
|
|
indices into the set of *surface* vertex positions. */
|
|
vector<Vector3<int>> surface_triangles;
|
|
surface_triangles.reserve(border_triangles.size());
|
|
for (auto& [triangle_key, face] : border_triangles) {
|
|
unused(triangle_key);
|
|
surface_triangles.emplace_back(volume_to_surface[face[0]],
|
|
volume_to_surface[face[1]],
|
|
volume_to_surface[face[2]]);
|
|
}
|
|
|
|
// TODO(xuchenhan-tri): Read the color of the mesh from properties.
|
|
return {g_id, inspector.GetName(g_id), move(surface_to_volume_vertices),
|
|
move(surface_triangles), volume_vertex_count};
|
|
}
|
|
|
|
// Simple class for converting shape specifications into LCM-compatible shapes.
|
|
class ShapeToLcm : public ShapeReifier {
|
|
public:
|
|
DRAKE_NO_COPY_NO_MOVE_NO_ASSIGN(ShapeToLcm)
|
|
|
|
ShapeToLcm() = default;
|
|
~ShapeToLcm() override = default;
|
|
|
|
lcmt_viewer_geometry_data Convert(const Shape& shape,
|
|
const RigidTransformd& X_PG,
|
|
const Rgba& in_color) {
|
|
X_PG_ = X_PG;
|
|
// NOTE: Reify *may* change X_PG_ based on the shape. For example, the
|
|
// half-space requires an additional offset to shift the box representing
|
|
// the plane *to* the plane.
|
|
shape.Reify(this);
|
|
|
|
// Saves the location and orientation of the visualization geometry in the
|
|
// `lcmt_viewer_geometry_data` object. The location and orientation are
|
|
// specified in the body's frame.
|
|
Eigen::Map<Eigen::Vector3f> position(geometry_data_.position);
|
|
position = X_PG_.translation().cast<float>();
|
|
// LCM quaternion must be w, x, y, z.
|
|
Quaterniond q(X_PG_.rotation().ToQuaternion());
|
|
geometry_data_.quaternion[0] = q.w();
|
|
geometry_data_.quaternion[1] = q.x();
|
|
geometry_data_.quaternion[2] = q.y();
|
|
geometry_data_.quaternion[3] = q.z();
|
|
|
|
Eigen::Map<Eigen::Vector4f> color(geometry_data_.color);
|
|
Eigen::Vector4d color_vec(in_color.r(), in_color.g(), in_color.b(),
|
|
in_color.a());
|
|
color = color_vec.cast<float>();
|
|
return geometry_data_;
|
|
}
|
|
|
|
using ShapeReifier::ImplementGeometry;
|
|
|
|
void ImplementGeometry(const Box& box, void*) override {
|
|
geometry_data_.type = geometry_data_.BOX;
|
|
geometry_data_.num_float_data = 3;
|
|
// Box width, depth, and height.
|
|
geometry_data_.float_data.push_back(static_cast<float>(box.width()));
|
|
geometry_data_.float_data.push_back(static_cast<float>(box.depth()));
|
|
geometry_data_.float_data.push_back(static_cast<float>(box.height()));
|
|
}
|
|
|
|
void ImplementGeometry(const Capsule& capsule, void*) override {
|
|
geometry_data_.type = geometry_data_.CAPSULE;
|
|
geometry_data_.num_float_data = 2;
|
|
geometry_data_.float_data.push_back(static_cast<float>(capsule.radius()));
|
|
geometry_data_.float_data.push_back(static_cast<float>(capsule.length()));
|
|
}
|
|
|
|
// For visualization, Convex is the same as Mesh.
|
|
void ImplementGeometry(const Convex& mesh, void*) override {
|
|
geometry_data_.type = geometry_data_.MESH;
|
|
geometry_data_.num_float_data = 3;
|
|
geometry_data_.float_data.push_back(static_cast<float>(mesh.scale()));
|
|
geometry_data_.float_data.push_back(static_cast<float>(mesh.scale()));
|
|
geometry_data_.float_data.push_back(static_cast<float>(mesh.scale()));
|
|
geometry_data_.string_data = mesh.filename();
|
|
}
|
|
|
|
void ImplementGeometry(const Cylinder& cylinder, void*) override {
|
|
geometry_data_.type = geometry_data_.CYLINDER;
|
|
geometry_data_.num_float_data = 2;
|
|
geometry_data_.float_data.push_back(static_cast<float>(cylinder.radius()));
|
|
geometry_data_.float_data.push_back(static_cast<float>(cylinder.length()));
|
|
}
|
|
|
|
void ImplementGeometry(const Ellipsoid& ellipsoid, void*) override {
|
|
geometry_data_.type = geometry_data_.ELLIPSOID;
|
|
geometry_data_.num_float_data = 3;
|
|
geometry_data_.float_data.push_back(static_cast<float>(ellipsoid.a()));
|
|
geometry_data_.float_data.push_back(static_cast<float>(ellipsoid.b()));
|
|
geometry_data_.float_data.push_back(static_cast<float>(ellipsoid.c()));
|
|
}
|
|
|
|
void ImplementGeometry(const HalfSpace&, void*) override {
|
|
// Currently representing a half space as a big box. This assumes that the
|
|
// underlying box representation is centered on the origin.
|
|
geometry_data_.type = geometry_data_.BOX;
|
|
geometry_data_.num_float_data = 3;
|
|
// Box width, height, and thickness.
|
|
geometry_data_.float_data.push_back(50);
|
|
geometry_data_.float_data.push_back(50);
|
|
const float thickness = 1;
|
|
geometry_data_.float_data.push_back(thickness);
|
|
|
|
// The final pose of the box is the half-space's pose pre-multiplied by
|
|
// an offset sufficient to move the box down so it's top face lies on the
|
|
// z = 0 plane.
|
|
// Shift it down so that the origin lies on the top surface.
|
|
RigidTransformd box_xform{Eigen::Vector3d{0, 0, -thickness / 2}};
|
|
X_PG_ = X_PG_ * box_xform;
|
|
}
|
|
|
|
void ImplementGeometry(const Mesh& mesh, void*) override {
|
|
geometry_data_.type = geometry_data_.MESH;
|
|
geometry_data_.num_float_data = 3;
|
|
geometry_data_.float_data.push_back(static_cast<float>(mesh.scale()));
|
|
geometry_data_.float_data.push_back(static_cast<float>(mesh.scale()));
|
|
geometry_data_.float_data.push_back(static_cast<float>(mesh.scale()));
|
|
geometry_data_.string_data = mesh.filename();
|
|
}
|
|
|
|
void ImplementGeometry(const Sphere& sphere, void*) override {
|
|
geometry_data_.type = geometry_data_.SPHERE;
|
|
geometry_data_.num_float_data = 1;
|
|
geometry_data_.float_data.push_back(static_cast<float>(sphere.radius()));
|
|
}
|
|
|
|
private:
|
|
lcmt_viewer_geometry_data geometry_data_{};
|
|
// The transform from the geometry frame to its parent frame.
|
|
RigidTransformd X_PG_;
|
|
};
|
|
|
|
} // namespace
|
|
|
|
namespace internal {
|
|
std::string MakeLcmChannelNameForRole(const std::string& channel,
|
|
const DrakeVisualizerParams& params) {
|
|
if (!params.use_role_channel_suffix) {
|
|
return channel;
|
|
}
|
|
DRAKE_DEMAND(params.role != Role::kUnassigned);
|
|
|
|
// These channel name transformations must be kept in sync with message
|
|
// consumers (meldis, drake_visualizer) in order for visualization of all
|
|
// roles to work.
|
|
switch (params.role) {
|
|
case Role::kIllustration:
|
|
return channel + "_ILLUSTRATION";
|
|
case Role::kProximity:
|
|
return channel + "_PROXIMITY";
|
|
case Role::kPerception:
|
|
return channel + "_PERCEPTION";
|
|
case Role::kUnassigned:
|
|
DRAKE_UNREACHABLE();
|
|
}
|
|
DRAKE_UNREACHABLE();
|
|
}
|
|
} // namespace internal
|
|
|
|
template <typename T>
|
|
DrakeVisualizer<T>::DrakeVisualizer(lcm::DrakeLcmInterface* lcm,
|
|
DrakeVisualizerParams params)
|
|
: DrakeVisualizer(lcm, std::move(params), true) {}
|
|
|
|
template <typename T>
|
|
template <typename U>
|
|
DrakeVisualizer<T>::DrakeVisualizer(const DrakeVisualizer<U>& other)
|
|
: DrakeVisualizer(nullptr, other.params_, false) {
|
|
DRAKE_DEMAND(owned_lcm_ == nullptr);
|
|
DRAKE_DEMAND(lcm_ == nullptr);
|
|
const lcm::DrakeLcm* owned_lcm =
|
|
dynamic_cast<const lcm::DrakeLcm*>(other.owned_lcm_.get());
|
|
if (owned_lcm == nullptr) {
|
|
throw std::runtime_error(
|
|
"DrakeVisualizer can only be scalar converted if it owns its "
|
|
"DrakeLcmInterface instance.");
|
|
}
|
|
owned_lcm_ = make_unique<lcm::DrakeLcm>(owned_lcm->get_lcm_url());
|
|
lcm_ = owned_lcm_.get();
|
|
}
|
|
|
|
template <typename T>
|
|
const DrakeVisualizer<T>& DrakeVisualizer<T>::AddToBuilder(
|
|
systems::DiagramBuilder<T>* builder, const SceneGraph<T>& scene_graph,
|
|
lcm::DrakeLcmInterface* lcm, DrakeVisualizerParams params) {
|
|
return AddToBuilder(builder, scene_graph.get_query_output_port(), lcm,
|
|
std::move(params));
|
|
}
|
|
|
|
template <typename T>
|
|
const DrakeVisualizer<T>& DrakeVisualizer<T>::AddToBuilder(
|
|
systems::DiagramBuilder<T>* builder,
|
|
const systems::OutputPort<T>& query_object_port,
|
|
lcm::DrakeLcmInterface* lcm, DrakeVisualizerParams params) {
|
|
const std::string aspirational_name =
|
|
fmt::format("drake_visualizer({})", params.role);
|
|
auto& visualizer =
|
|
*builder->template AddSystem<DrakeVisualizer<T>>(lcm, std::move(params));
|
|
if (!builder->HasSubsystemNamed(aspirational_name)) {
|
|
visualizer.set_name(aspirational_name);
|
|
}
|
|
builder->Connect(query_object_port, visualizer.query_object_input_port());
|
|
return visualizer;
|
|
}
|
|
|
|
template <typename T>
|
|
void DrakeVisualizer<T>::DispatchLoadMessage(const SceneGraph<T>& scene_graph,
|
|
lcm::DrakeLcmInterface* lcm,
|
|
DrakeVisualizerParams params) {
|
|
DRAKE_DEMAND(lcm != nullptr);
|
|
vector<internal::DynamicFrameData> dynamic_frames;
|
|
PopulateDynamicFrameData(scene_graph.model_inspector(), params,
|
|
&dynamic_frames);
|
|
SendLoadNonDeformableMessage(scene_graph.model_inspector(), params,
|
|
dynamic_frames, 0, lcm);
|
|
}
|
|
|
|
template <typename T>
|
|
DrakeVisualizer<T>::DrakeVisualizer(lcm::DrakeLcmInterface* lcm,
|
|
DrakeVisualizerParams params, bool use_lcm)
|
|
: systems::LeafSystem<T>(SystemTypeTag<DrakeVisualizer>{}),
|
|
owned_lcm_((lcm || !use_lcm) ? nullptr : new lcm::DrakeLcm()),
|
|
lcm_((lcm && use_lcm) ? lcm : owned_lcm_.get()),
|
|
params_(std::move(params)) {
|
|
// This constructor should not do anything that requires lcm_ (or owned_lcm_)
|
|
// to be non-nullptr. By design, both being nullptr is a desired, expected
|
|
// possibility during the scope of the constructor.
|
|
if (params_.publish_period <= 0) {
|
|
throw std::runtime_error(fmt::format(
|
|
"DrakeVisualizer requires a positive publish period; {} was given",
|
|
params_.publish_period));
|
|
}
|
|
if (params_.role == Role::kUnassigned) {
|
|
throw std::runtime_error(
|
|
"DrakeVisualizer cannot be used for geometries with the "
|
|
"Role::kUnassigned value. Please choose proximity, perception, or "
|
|
"illustration");
|
|
}
|
|
|
|
this->DeclarePeriodicPublishEvent(params_.publish_period, 0.0,
|
|
&DrakeVisualizer<T>::SendGeometryMessage);
|
|
this->DeclareForcedPublishEvent(&DrakeVisualizer<T>::SendGeometryMessage);
|
|
|
|
query_object_input_port_ =
|
|
this->DeclareAbstractInputPort("query_object", Value<QueryObject<T>>())
|
|
.get_index();
|
|
|
|
// These cache entries depend on *nothing*.
|
|
frame_data_cache_index_ =
|
|
this->DeclareCacheEntry("dynamic_frames",
|
|
&DrakeVisualizer<T>::CalcDynamicFrameData,
|
|
{this->nothing_ticket()})
|
|
.cache_index();
|
|
deformable_data_cache_index_ =
|
|
this->DeclareCacheEntry("deformable_data",
|
|
&DrakeVisualizer<T>::CalcDeformableMeshData,
|
|
{this->nothing_ticket()})
|
|
.cache_index();
|
|
}
|
|
|
|
template <typename T>
|
|
EventStatus DrakeVisualizer<T>::SendGeometryMessage(
|
|
const Context<T>& context) const {
|
|
const auto& query_object =
|
|
query_object_input_port().template Eval<QueryObject<T>>(context);
|
|
const GeometryVersion& current_version =
|
|
query_object.inspector().geometry_version();
|
|
|
|
bool send_load_message = false;
|
|
{
|
|
std::lock_guard<std::mutex> lock(mutex_);
|
|
if (!version_.IsSameAs(current_version, params_.role)) {
|
|
send_load_message = true;
|
|
version_ = current_version;
|
|
}
|
|
}
|
|
if (send_load_message) {
|
|
SendLoadNonDeformableMessage(
|
|
query_object.inspector(), params_, RefreshDynamicFrameData(context),
|
|
ExtractDoubleOrThrow(context.get_time()), lcm_);
|
|
RefreshDeformableMeshData(context);
|
|
}
|
|
|
|
SendDrawNonDeformableMessage(query_object, params_,
|
|
EvalDynamicFrameData(context),
|
|
ExtractDoubleOrThrow(context.get_time()), lcm_);
|
|
SendDeformableGeometriesMessage(
|
|
query_object, params_, EvalDeformableMeshData(context),
|
|
ExtractDoubleOrThrow(context.get_time()), lcm_);
|
|
|
|
return EventStatus::Succeeded();
|
|
}
|
|
|
|
template <typename T>
|
|
void DrakeVisualizer<T>::SendLoadNonDeformableMessage(
|
|
const SceneGraphInspector<T>& inspector,
|
|
const DrakeVisualizerParams& params,
|
|
const std::vector<internal::DynamicFrameData>& dynamic_frames, double time,
|
|
lcm::DrakeLcmInterface* lcm) {
|
|
lcmt_viewer_load_robot message{};
|
|
|
|
// Add the world frame if it has geometries with the specified role.
|
|
const int anchored_count = inspector.NumGeometriesForFrameWithRole(
|
|
inspector.world_frame_id(), params.role);
|
|
const int frame_count =
|
|
static_cast<int>(dynamic_frames.size()) + (anchored_count > 0 ? 1 : 0);
|
|
|
|
message.num_links = frame_count;
|
|
message.link.resize(frame_count);
|
|
|
|
// Helper utility to create lcm geometry description from geometry id.
|
|
auto make_geometry = [¶ms, &inspector](GeometryId g_id) {
|
|
const GeometryProperties* props =
|
|
inspector.GetProperties(g_id, params.role);
|
|
// We assume that the g_id was obtained by asking for geometries with the
|
|
// indicated role. So, by definition, the properties should be non-null.
|
|
DRAKE_ASSERT(props != nullptr);
|
|
const Shape& shape = inspector.GetShape(g_id);
|
|
Rgba default_color = params.default_color;
|
|
if (params.role != Role::kIllustration) {
|
|
const GeometryProperties* illus_props =
|
|
inspector.GetIllustrationProperties(g_id);
|
|
if (illus_props) {
|
|
default_color = illus_props->GetPropertyOrDefault("phong", "diffuse",
|
|
default_color);
|
|
}
|
|
}
|
|
const Rgba& color =
|
|
props->GetPropertyOrDefault("phong", "diffuse", default_color);
|
|
if (params.role == Role::kProximity && params.show_hydroelastic) {
|
|
auto hydro_mesh = inspector.maybe_get_hydroelastic_mesh(g_id);
|
|
if (!std::holds_alternative<std::monostate>(hydro_mesh)) {
|
|
// This Shape *definitely* has a mesh associated with it. Replace the
|
|
// primitive with the mesh hydroelastic mesh.
|
|
return MakeHydroMesh(g_id, inspector, inspector.GetPoseInFrame(g_id),
|
|
color);
|
|
}
|
|
}
|
|
return ShapeToLcm().Convert(shape, inspector.GetPoseInFrame(g_id), color);
|
|
};
|
|
|
|
int link_index = 0;
|
|
// Load anchored geometry into the world frame.
|
|
if (anchored_count) {
|
|
message.link[0].name = "world";
|
|
message.link[0].robot_num = 0;
|
|
message.link[0].num_geom = anchored_count;
|
|
message.link[0].geom.resize(anchored_count);
|
|
int geom_index = -1; // We'll pre-increment before using.
|
|
for (const GeometryId& g_id :
|
|
inspector.GetGeometries(inspector.world_frame_id(), params.role)) {
|
|
// Deformable geometries are handled via
|
|
// SendDeformableGeometriesMessage().
|
|
if (!inspector.IsDeformableGeometry(g_id)) {
|
|
message.link[0].geom[++geom_index] = make_geometry(g_id);
|
|
}
|
|
}
|
|
link_index = 1;
|
|
}
|
|
|
|
// Load dynamic geometry into their own frames.
|
|
for (const auto& [frame_id, geometry_count, name] : dynamic_frames) {
|
|
message.link[link_index].name = name;
|
|
message.link[link_index].robot_num = inspector.GetFrameGroup(frame_id);
|
|
message.link[link_index].num_geom = geometry_count;
|
|
message.link[link_index].geom.resize(geometry_count);
|
|
int geom_index = -1; // We'll pre-increment before using.
|
|
for (const GeometryId& g_id :
|
|
inspector.GetGeometries(frame_id, params.role)) {
|
|
message.link[link_index].geom[++geom_index] = make_geometry(g_id);
|
|
}
|
|
++link_index;
|
|
}
|
|
|
|
|
|
std::string channel = MakeLcmChannelNameForRole("DRAKE_VIEWER_LOAD_ROBOT",
|
|
params);
|
|
lcm::Publish(lcm, channel, message, time);
|
|
}
|
|
|
|
template <typename T>
|
|
void DrakeVisualizer<T>::SendDrawNonDeformableMessage(
|
|
const QueryObject<T>& query_object,
|
|
const DrakeVisualizerParams& params,
|
|
const vector<internal::DynamicFrameData>& dynamic_frames, double time,
|
|
lcm::DrakeLcmInterface* lcm) {
|
|
lcmt_viewer_draw message{};
|
|
|
|
const int frame_count = static_cast<int>(dynamic_frames.size());
|
|
|
|
message.timestamp = static_cast<int64_t>(time * 1000.0);
|
|
message.num_links = frame_count;
|
|
message.link_name.resize(frame_count);
|
|
message.robot_num.resize(frame_count);
|
|
message.position.resize(frame_count);
|
|
message.quaternion.resize(frame_count);
|
|
|
|
const SceneGraphInspector<T>& inspector = query_object.inspector();
|
|
for (int i = 0; i < frame_count; ++i) {
|
|
const FrameId frame_id = dynamic_frames[i].frame_id;
|
|
message.robot_num[i] = inspector.GetFrameGroup(frame_id);
|
|
message.link_name[i] = dynamic_frames[i].name;
|
|
|
|
const math::RigidTransformd& X_WF =
|
|
internal::convert_to_double(query_object.GetPoseInWorld(frame_id));
|
|
message.position[i].resize(3);
|
|
message.position[i][0] = X_WF.translation()[0];
|
|
message.position[i][1] = X_WF.translation()[1];
|
|
message.position[i][2] = X_WF.translation()[2];
|
|
|
|
const Eigen::Quaternion<double> q = X_WF.rotation().ToQuaternion();
|
|
message.quaternion[i].resize(4);
|
|
message.quaternion[i][0] = q.w();
|
|
message.quaternion[i][1] = q.x();
|
|
message.quaternion[i][2] = q.y();
|
|
message.quaternion[i][3] = q.z();
|
|
}
|
|
|
|
std::string channel = MakeLcmChannelNameForRole("DRAKE_VIEWER_DRAW",
|
|
params);
|
|
lcm::Publish(lcm, channel, message, time);
|
|
}
|
|
|
|
template <typename T>
|
|
void DrakeVisualizer<T>::SendDeformableGeometriesMessage(
|
|
const QueryObject<T>& query_object, const DrakeVisualizerParams& params,
|
|
const vector<internal::DeformableMeshData>& deformable_data, double time,
|
|
lcm::DrakeLcmInterface* lcm) {
|
|
lcmt_viewer_link_data message{};
|
|
message.name = "deformable_geometries";
|
|
message.robot_num = 0; // robot_num = 0 corresponds to world frame.
|
|
message.num_geom = deformable_data.size();
|
|
message.geom.resize(message.num_geom);
|
|
for (int i = 0; i < message.num_geom; ++i) {
|
|
const internal::DeformableMeshData& data = deformable_data[i];
|
|
const GeometryId g_id = data.geometry_id;
|
|
const VectorX<T>& vertex_positions =
|
|
query_object.GetConfigurationsInWorld(g_id);
|
|
if (vertex_positions.size() <= data.volume_vertex_count) {
|
|
throw std::logic_error(fmt::format(
|
|
"For mesh named '{}', The number of given vertex positions "
|
|
"({}) is smaller than the "
|
|
"minimum expected number of positions ({}).",
|
|
data.name, vertex_positions.size(), data.volume_vertex_count));
|
|
}
|
|
// TODO(xuchenhan-tri): We should use the color from the property of the
|
|
// geometry when available.
|
|
message.geom[i] =
|
|
MakeDeformableSurfaceMesh(vertex_positions, data, params.default_color);
|
|
}
|
|
std::string channel = MakeLcmChannelNameForRole("DRAKE_VIEWER_DEFORMABLE",
|
|
params);
|
|
lcm::Publish(lcm, channel, message, time);
|
|
}
|
|
|
|
template <typename T>
|
|
void DrakeVisualizer<T>::CalcDynamicFrameData(
|
|
const Context<T>& context,
|
|
vector<internal::DynamicFrameData>* frame_data) const {
|
|
const auto& query_object =
|
|
query_object_input_port().template Eval<QueryObject<T>>(context);
|
|
PopulateDynamicFrameData(query_object.inspector(), params_, frame_data);
|
|
}
|
|
|
|
template <typename T>
|
|
const vector<internal::DynamicFrameData>&
|
|
DrakeVisualizer<T>::RefreshDynamicFrameData(const Context<T>& context) const {
|
|
// We'll need to make sure our knowledge of dynamic frames can get updated.
|
|
this->get_cache_entry(frame_data_cache_index_)
|
|
.get_mutable_cache_entry_value(context)
|
|
.mark_out_of_date();
|
|
|
|
return EvalDynamicFrameData(context);
|
|
}
|
|
|
|
template <typename T>
|
|
const vector<internal::DynamicFrameData>&
|
|
DrakeVisualizer<T>::EvalDynamicFrameData(const Context<T>& context) const {
|
|
return this->get_cache_entry(frame_data_cache_index_)
|
|
.template Eval<vector<internal::DynamicFrameData>>(context);
|
|
}
|
|
|
|
template <typename T>
|
|
void DrakeVisualizer<T>::PopulateDynamicFrameData(
|
|
const SceneGraphInspector<T>& inspector,
|
|
const DrakeVisualizerParams& params,
|
|
vector<internal::DynamicFrameData>* frame_data) {
|
|
// Collect the dynamic frames that actually have geometries of the
|
|
// specified role. These are the frames broadcast in a draw message and are
|
|
// also part of the load message (plus possibly the world frame).
|
|
vector<internal::DynamicFrameData>& dynamic_frames = *frame_data;
|
|
dynamic_frames.clear();
|
|
|
|
for (const FrameId& frame_id : inspector.GetAllFrameIds()) {
|
|
// We'll handle the world frame special.
|
|
if (frame_id == inspector.world_frame_id()) continue;
|
|
const int count =
|
|
inspector.NumGeometriesForFrameWithRole(frame_id, params.role);
|
|
if (count > 0) {
|
|
dynamic_frames.push_back({frame_id, count,
|
|
inspector.GetOwningSourceName(frame_id) +
|
|
"::" + inspector.GetName(frame_id)});
|
|
}
|
|
}
|
|
}
|
|
|
|
template <typename T>
|
|
void DrakeVisualizer<T>::CalcDeformableMeshData(
|
|
const Context<T>& context,
|
|
vector<internal::DeformableMeshData>* deformable_data) const {
|
|
DRAKE_DEMAND(deformable_data != nullptr);
|
|
deformable_data->clear();
|
|
const auto& query_object =
|
|
query_object_input_port().template Eval<QueryObject<T>>(context);
|
|
const auto& inspector = query_object.inspector();
|
|
const std::vector<GeometryId> deformable_geometries =
|
|
inspector.GetAllDeformableGeometryIds();
|
|
for (const auto g_id : deformable_geometries) {
|
|
deformable_data->emplace_back(MakeDeformableMeshData(g_id, inspector));
|
|
}
|
|
}
|
|
|
|
template <typename T>
|
|
const vector<internal::DeformableMeshData>&
|
|
DrakeVisualizer<T>::RefreshDeformableMeshData(const Context<T>& context) const {
|
|
// We'll need to make sure our knowledge of deformable data can get updated.
|
|
this->get_cache_entry(deformable_data_cache_index_)
|
|
.get_mutable_cache_entry_value(context)
|
|
.mark_out_of_date();
|
|
|
|
return EvalDeformableMeshData(context);
|
|
}
|
|
|
|
template <typename T>
|
|
const vector<internal::DeformableMeshData>&
|
|
DrakeVisualizer<T>::EvalDeformableMeshData(const Context<T>& context) const {
|
|
return this->get_cache_entry(deformable_data_cache_index_)
|
|
.template Eval<vector<internal::DeformableMeshData>>(context);
|
|
}
|
|
|
|
} // namespace geometry
|
|
} // namespace drake
|
|
|
|
DRAKE_DEFINE_CLASS_TEMPLATE_INSTANTIATIONS_ON_DEFAULT_NONSYMBOLIC_SCALARS(
|
|
class ::drake::geometry::DrakeVisualizer)
|