Mujoco C++ : Visionneuse de scène minimale avec navigation à la souris et simulation

Ce programme Mujoco C++ minimal charge une scène Mujoco depuis un fichier XML et l’affiche dans une fenêtre avec navigation à la souris. Il utilise GLFW pour la gestion de fenêtre et la gestion des entrées.

Outre la visualisation, il exécute également une boucle de simulation simple, faisant avancer la physique en temps réel. Notez que la simulation est très basique pour les besoins de cet exemple, mais elle intègre une synchronisation temps réel.

Mujoco minimal viewer

mujoco_scene_viewer.cpp
// minimal_viewer.cpp
// Compilation : voir les notes ci-dessous

#include <mujoco/mujoco.h>
#include <GLFW/glfw3.h>
#include <cstdio>
#include <cstdlib>
#include <cmath>
#include <time.h>

// --- Globales réduites pour les callbacks GLFW ---
static mjModel* m = nullptr;
static mjData*  d = nullptr;

static mjvCamera cam;       // caméra abstraite
static mjvOption opt;       // options de visualisation
static mjvScene  scn;       // scène abstraite
static mjrContext con;      // contexte GPU personnalisé

static bool  button_left = false, button_middle = false, button_right = false;
static double lastx = 0, lasty = 0;
// Synchronisation temps réel : enregistrer les temps de départ de simulation et d'horloge
static double sim_start_time = 0.0;
static double realtime_start = 0.0;

// --- Callback d'erreur GLFW ---
static void glfw_error_callback(int error, const char* description) {
  std::fprintf(stderr, "Erreur GLFW %d : %s\n", error, description);
}

// --- Bouton de la souris : mémoriser quels boutons sont enfoncés ---
static void mouse_button_callback(GLFWwindow* window, int button, int act, int /*mods*/) {
  // Gestion simple et stable de l'état des boutons
  if (button == GLFW_MOUSE_BUTTON_LEFT)   button_left   = (act == GLFW_PRESS) ? true : (act == GLFW_RELEASE ? false : button_left);
  if (button == GLFW_MOUSE_BUTTON_MIDDLE) button_middle = (act == GLFW_PRESS) ? true : (act == GLFW_RELEASE ? false : button_middle);
  if (button == GLFW_MOUSE_BUTTON_RIGHT)  button_right  = (act == GLFW_PRESS) ? true : (act == GLFW_RELEASE ? false : button_right);

  // Enregistrer la position actuelle du curseur (coordonnées fenêtre)
  glfwGetCursorPos(window, &lastx, &lasty);
}

// --- Déplacement de la souris : convertir le mouvement en action de caméra ---
static void cursor_pos_callback(GLFWwindow* window, double xpos, double ypos) {
  if (!m) return;
  // Si aucun bouton de la souris n'est enfoncé, juste mettre à jour la dernière position
  if (!button_left && !button_right && !button_middle) {
    lastx = xpos;
    lasty = ypos;
    return;
  }

  // Calculer le mouvement en coordonnées fenêtre, normaliser par la hauteur du framebuffer (stable selon la DPI)
  int fbw = 0, fbh = 1;
  glfwGetFramebufferSize(window, &fbw, &fbh);
  if (fbh <= 0) fbh = 1;

  double dx = (xpos - lastx) / (double)fbh;
  double dy = (ypos - lasty) / (double)fbh;
  lastx = xpos;
  lasty = ypos;

  if (button_left) {
    // Glisser-gauche : rotation
    mjv_moveCamera(m, mjMOUSE_ROTATE_H, dx, dy, &scn, &cam);
  } else if (button_right) {
    // Glisser-droit : déplacement horizontal
    mjv_moveCamera(m, mjMOUSE_MOVE_H, dx, dy, &scn, &cam);
  } else if (button_middle) {
    // Glisser-milieu : zoom
    mjv_moveCamera(m, mjMOUSE_ZOOM, dx, dy, &scn, &cam);
  }
}

// --- Molette de défilement : zoom ---
static void scroll_callback(GLFWwindow* window, double /*xoffset*/, double yoffset) {
  if (!m) return;
  // Défilement pour zoomer (facteur doux)
  mjv_moveCamera(m, mjMOUSE_ZOOM, 0, -0.05 * yoffset, &scn, &cam);
}

// --- Touche : 'R' pour réinitialiser ---
static void key_callback(GLFWwindow* window, int key, int scancode, int act, int mods) {
  if (act == GLFW_PRESS || act == GLFW_REPEAT) {
    if (key == GLFW_KEY_R) {
      // Réinitialiser l'état de simulation et les minuteurs temps réel pour que la sim n'avance pas trop
      mj_resetData(m, d);
      sim_start_time = (d ? d->time : 0.0);
      realtime_start = glfwGetTime();
    }
    if (key == GLFW_KEY_ESCAPE) glfwSetWindowShouldClose(window, GLFW_TRUE);
  }
}

int main(int argc, char** argv) {
  // Utilisation
  if (argc < 2) {
    std::printf("Utilisation : %s modele.xml\n", argv[0]);
    return 1;
  }

  // (MuJoCo open-source >= 2.2 n'a pas d'étape d'activation de licence)

  // Charger le modèle
  char error[1024] = {0};
  m = mj_loadXML(argv[1], nullptr, error, sizeof(error));
  if (!m) {
    std::fprintf(stderr, "Impossible de charger le modèle : %s\n", error);
    return 1;
  }
  d = mj_makeData(m);

  // Initialiser GLFW
  glfwSetErrorCallback(glfw_error_callback);
  if (!glfwInit()) {
    std::fprintf(stderr, "Impossible d'initialiser GLFW\n");
    return 1;
  }

  // Créer la fenêtre/le contexte
  glfwWindowHint(GLFW_DOUBLEBUFFER, 1);
  GLFWwindow* window = glfwCreateWindow(1200, 900, "MuJoCo Minimal Viewer", nullptr, nullptr);
  if (!window) {
    std::fprintf(stderr, "Impossible de créer la fenêtre GLFW\n");
    glfwTerminate();
    return 1;
  }
  glfwMakeContextCurrent(window);
  glfwSwapInterval(0); // désactiver vsync

  // Définir les callbacks
  glfwSetMouseButtonCallback(window, mouse_button_callback);
  glfwSetCursorPosCallback(window, cursor_pos_callback);
  glfwSetScrollCallback(window, scroll_callback);
  glfwSetKeyCallback(window, key_callback);

  // Initialiser la visualisation MuJoCo
  mjv_defaultCamera(&cam);
  // Définir le point de visée de la caméra pour que la vue initiale soit centrée sur le modèle
  cam.lookat[0] = 0.0;
  cam.lookat[1] = 0.0;
  cam.lookat[2] = 0.5;
  mjv_defaultOption(&opt);
  mjv_defaultScene(&scn);
  mjr_defaultContext(&con);

  mjv_makeScene(m, &scn, 2000);               // allouer la scène
  mjr_makeContext(m, &con, mjFONTSCALE_100);  // allouer le contexte GPU

  // Initialiser les minuteurs de synchronisation temps réel (démarrer les deux à la même heure d'horloge)
  sim_start_time = d->time;
  realtime_start = glfwGetTime();

  // Placer la caméra pour afficher tout le modèle
  cam.type = mjCAMERA_FREE;
  mj_forward(m, d);

  // Boucle principale
  while (!glfwWindowShouldClose(window)) {
    // Faire avancer la simulation (pas simple approximativement temps réel)
    mj_step(m, d);

    // --- Synchronisation temps réel : s'assurer que la sim ne va pas plus vite que l'horloge ---
    double sim_elapsed = d->time - sim_start_time;                 // secondes de simulation depuis le départ/la réinitialisation
    double real_elapsed = glfwGetTime() - realtime_start;          // secondes d'horloge depuis le départ/la réinitialisation
    double sleep_time = sim_elapsed - real_elapsed;                // positif => la sim est en avance
    if (sleep_time > 0.0) {
      // Dormir le temps nécessaire (nanosleep est sûr sans options de linker de threading)
      struct timespec req;
      req.tv_sec = (time_t)std::floor(sleep_time);
      req.tv_nsec = (long)((sleep_time - req.tv_sec) * 1e9);
      if (req.tv_sec < 0) req.tv_sec = 0;
      if (req.tv_nsec < 0) req.tv_nsec = 0;
      nanosleep(&req, nullptr);
    }

    // Obtenir la taille du framebuffer et définir le viewport
    int width, height;
    glfwGetFramebufferSize(window, &width, &height);
    mjrRect viewport = {0, 0, width, height};

    // Mettre à jour la scène et rendre
    mjv_updateScene(m, d, &opt, nullptr, &cam, mjCAT_ALL, &scn);
    mjr_render(viewport, &scn, &con);

    // Échanger et sonder
    glfwSwapBuffers(window);
    glfwPollEvents();
  }

  // Nettoyage
  mjr_freeContext(&con);
  mjv_freeScene(&scn);
  mj_deleteData(d);
  mj_deleteModel(m);
  glfwTerminate();
  return 0;
}

Comment compiler

Enregistrez ceci sous CMakeLists.txt

CMakeLists.txt
cmake_minimum_required(VERSION 3.10)
project(mujocotest)

set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_CXX_EXTENSIONS OFF)

find_package(PkgConfig REQUIRED)
pkg_search_module(MUJOCO mujoco)
pkg_search_module(GLFW REQUIRED glfw3)

if(NOT MUJOCO_FOUND)
	set(MUJOCO_INCLUDE_DIRS /opt/mujoco/include)
	set(MUJOCO_LIBRARIES /opt/mujoco/lib/libmujoco.so)
endif()

add_executable(mujocotest main.cpp)
target_include_directories(mujocotest PRIVATE ${MUJOCO_INCLUDE_DIRS} ${GLFW_INCLUDE_DIRS})
target_link_libraries(mujocotest PRIVATE ${MUJOCO_LIBRARIES} ${GLFW_LIBRARIES})

et compilez comme ceci :

build_mujoco.sh
cmake .
make -j8

Comment exécuter

run_mujoco.sh
./mujocotest ~/mujoco_menagerie/franka_fr3/fr3.xml

Check out similar posts by category: Mujoco, Robotics, C++