Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
7e2efe8
[All] Adds video recorder
EulalieCoevoet Feb 27, 2026
7731293
records in screenshots folder
EulalieCoevoet Feb 27, 2026
58c2d1e
generalizes generated filename with date and time
EulalieCoevoet Feb 27, 2026
ec9dfde
forgotten file
EulalieCoevoet Feb 27, 2026
0930080
minor esthetic changes and cleaning
EulalieCoevoet Feb 27, 2026
811fe9e
disabled in scene editor workbench
EulalieCoevoet Feb 27, 2026
20b0dc9
adds to viewmenu and viewport context menu
EulalieCoevoet Feb 27, 2026
787b853
duplicate code...
EulalieCoevoet Feb 27, 2026
d8eec04
Display recording statuseven when the camera buttons are collapsed
HanaeRateau Mar 3, 2026
633084d
[ComponentsWindow] list components examples (#102)
EulalieCoevoet Mar 4, 2026
f7c4abd
[SofaImGui] Widget: adds missing float widget (#103)
EulalieCoevoet Mar 4, 2026
11ac81d
[SofaGLFW] use ctrl+shift for keyboard events (#104)
EulalieCoevoet Mar 4, 2026
da84061
[SofaImGui] Format LocalTextLinkOpenURL and add useful links (#106)
EulalieCoevoet Mar 4, 2026
d6bec7f
[MouseManagerWindow] fixes crash (#107)
EulalieCoevoet Mar 4, 2026
767d703
[Widgets] refactoring and limits table vertical size in windows (#105)
EulalieCoevoet Mar 4, 2026
304c202
[SofaImGui] Cleaning simulation driving window (#109)
EulalieCoevoet Mar 4, 2026
c9216a7
[resources] adds new icons and fixes alignement
EulalieCoevoet Mar 5, 2026
1707907
Merge branch 'robotics' into pr_videorecorder
EulalieCoevoet Mar 6, 2026
a54df85
Merge branch 'robotics' into pr_videorecorder
EulalieCoevoet Mar 9, 2026
caaf8d5
adds RecordVideoWindow
EulalieCoevoet Mar 9, 2026
157c32f
Merge branch 'robotics' into pr_videorecorder
EulalieCoevoet Mar 16, 2026
7af9692
review requested changes: adds message on footer status bar in failure
EulalieCoevoet Mar 16, 2026
58e5043
Apply suggestion from @EulalieCoevoet
EulalieCoevoet Mar 16, 2026
622a86f
Adds error footer message if no file has been recorded
HanaeRateau Mar 17, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 33 additions & 0 deletions SofaGLFW/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -82,8 +82,41 @@ if(SofaPython3_FOUND)
add_subdirectory(bindings)
endif()

if(TARGET PkgConfig::FFMPEG)
target_link_libraries(${PROJECT_NAME} PRIVATE PkgConfig::FFMPEG)
set(SOFAGLFW_HAVE_FFMPEG 1)
endif()

add_definitions("-DSOFAGLFW_RESOURCES_DIR=\"${CMAKE_CURRENT_SOURCE_DIR}/resources\"")

# FFMPEG_exec
sofa_find_package(FFMPEG_exec BOTH_SCOPES)
# FFMPEG
if(FFMPEG_EXEC_FOUND)
install(PROGRAMS "${FFMPEG_EXEC_FILE}" DESTINATION ${CMAKE_INSTALL_PREFIX}/bin COMPONENT applications)
endif()

# Create build and install versions of .ini file for resources finding
## For build tree
set(FFMPEG_EXEC_PATH "${FFMPEG_EXEC_FILE}") # absolute path for build dir, see .ini file
configure_file(${CMAKE_CURRENT_SOURCE_DIR}/etc/${PROJECT_NAME}.ini.in "${CMAKE_BINARY_DIR}/etc/${PROJECT_NAME}.ini")

## For install tree
get_filename_component(FFMPEG_EXEC_FILENAME "${FFMPEG_EXEC_FILE}" NAME)
if(FFMPEG_EXEC_FILENAME)
set(FFMPEG_EXEC_PATH "../bin/${FFMPEG_EXEC_FILENAME}")
else()
#If ffmpeg hasn't been found, it'll not be shipped but we still want to offer the possibility for the user to use it
#easily by putting the executable alongside runSofa
set(FFMPEG_EXEC_PATH "../bin/ffmpeg")
if(WIN32)
set(FFMPEG_EXEC_PATH "${FFMPEG_EXEC_PATH}.exe")
endif()
endif()
configure_file(${CMAKE_CURRENT_SOURCE_DIR}/etc/${PROJECT_NAME}.ini.in "${CMAKE_BINARY_DIR}/etc/installed${PROJECT_NAME}.ini")
#Hack relative tree to ../../etc because of Windows:NSIS that doesn't allow absolute install path.
install(FILES "${CMAKE_BINARY_DIR}/etc/installed${PROJECT_NAME}.ini" DESTINATION ../../etc RENAME ${PROJECT_NAME}.ini COMPONENT applications)

sofa_create_package_with_targets(
PACKAGE_NAME ${PROJECT_NAME}
PACKAGE_VERSION ${Sofa_VERSION}
Expand Down
25 changes: 19 additions & 6 deletions SofaGLFW/SofaGLFWConfig.cmake.in
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,27 @@
@PACKAGE_GUARD@
@PACKAGE_INIT@

find_package(Sofa.Config REQUIRED)
find_package(Sofa.Simulation.Graph REQUIRED)
find_package(Sofa.GL REQUIRED)
find_package(Sofa.Component.Visual REQUIRED)
find_package(Sofa.GUI.Common QUIET)
find_package(Sofa.Config QUIET REQUIRED)
find_package(Sofa.Simulation.Graph QUIET REQUIRED)
find_package(Sofa.GL QUIET REQUIRED)
find_package(Sofa.Component.Visual QUIET REQUIRED)

if(NOT TARGET glfw)
sofa_find_package(glfw3 QUIET REQUIRED)
endif()

set(SOFAGLFW_HAVE_SOFA_GUI_COMMON @SOFAGLFW_HAVE_SOFA_GUI_COMMON@)
if(SOFAGLFW_HAVE_SOFA_GUI_COMMON)
find_package(Sofa.GUI.Common QUIET REQUIRED)
endif()

set(SOFAGLFW_HAVE_SOFAPYTHON3 @SOFAGLFW_HAVE_SOFAPYTHON3@)
if(SOFAGLFW_HAVE_SOFAPYTHON3)
find_package(SofaPython3 QUIET REQUIRED)
endif()

if(NOT TARGET @PROJECT_NAME@)
include("${CMAKE_CURRENT_LIST_DIR}/@PROJECT_NAME@Targets.cmake")
endif()

check_required_components(@PROJECT_NAME@)
check_required_components(@PROJECT_NAME@)
1 change: 1 addition & 0 deletions SofaGLFW/etc/SofaGLFW.ini.in
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
FFMPEG_EXEC_PATH=@FFMPEG_EXEC_PATH@
4 changes: 4 additions & 0 deletions SofaGLFW/src/SofaGLFW/BaseGUIEngine.h
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,9 @@
#include <SofaGLFW/config.h>
#include <sofa/simulation/Node.h>

#include <sofa/type/fwd.h>
#include <vector>

struct GLFWwindow;

namespace sofaglfw
Expand All @@ -42,6 +45,7 @@ class SOFAGLFW_API BaseGUIEngine
virtual void afterDraw() = 0;
virtual void terminate() = 0;
virtual bool dispatchMouseEvents() = 0;
virtual sofa::type::Vec2i getFrameBufferPixels(std::vector<uint8_t>& pixels) = 0;

virtual void key_callback(GLFWwindow* window, int key, int scancode, int action, int mods) {SOFA_UNUSED(window); SOFA_UNUSED(key); SOFA_UNUSED(scancode); SOFA_UNUSED(action); SOFA_UNUSED(mods);}

Expand Down
10 changes: 10 additions & 0 deletions SofaGLFW/src/SofaGLFW/NullGUIEngine.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -61,4 +61,14 @@ bool NullGUIEngine::dispatchMouseEvents()
return true;
}

sofa::type::Vec2i NullGUIEngine::getFrameBufferPixels(std::vector<uint8_t>& pixels)
{
GLint viewport[4];
glGetIntegerv(GL_VIEWPORT, viewport);
pixels.resize(viewport[2] * viewport[3] * 3);
glReadPixels(viewport[0], viewport[1], viewport[2], viewport[3], GL_RGB, GL_UNSIGNED_BYTE, pixels.data());

return {viewport[2], viewport[3]};
}

} // namespace sofaglfw
1 change: 1 addition & 0 deletions SofaGLFW/src/SofaGLFW/NullGUIEngine.h
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ class NullGUIEngine : public BaseGUIEngine
void afterDraw() override {}
void terminate() override;
bool dispatchMouseEvents() override;
sofa::type::Vec2i getFrameBufferPixels(std::vector<uint8_t>& pixels) override;
};

} // namespace sofaglfw
158 changes: 140 additions & 18 deletions SofaGLFW/src/SofaGLFW/SofaGLFWBaseGUI.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -20,33 +20,40 @@
* Contact information: contact@sofa-framework.org *
******************************************************************************/

#include <sofa/gui/common/PickHandler.h>
#include <SofaGLFW/SofaGLFWBaseGUI.h>

#include <filesystem>
#define GLFW_INCLUDE_NONE
#include <GLFW/glfw3.h>

#include <sofa/gui/common/BaseGUI.h>
#include <SofaGLFW/SofaGLFWBaseGUI.h>
#include <SofaGLFW/SofaGLFWWindow.h>

#include <sofa/helper/logging/Messaging.h>
#include <sofa/helper/AdvancedTimer.h>
#include <sofa/gui/common/PickHandler.h>

#include <sofa/simulation/SimulationLoop.h>
#include <sofa/simulation/Node.h>
#include <sofa/simulation/Simulation.h>
#include <sofa/core/visual/VisualParams.h>
#include <sofa/core/objectmodel/BaseClassNameHelper.h>

#include <sofa/component/visual/InteractiveCamera.h>
#include <sofa/component/visual/VisualStyle.h>

#include <sofa/core/visual/VisualParams.h>
#include <sofa/core/objectmodel/BaseClassNameHelper.h>
#include <sofa/core/objectmodel/KeypressedEvent.h>
#include <sofa/core/objectmodel/KeyreleasedEvent.h>

#include <sofa/helper/io/STBImage.h>
#include <sofa/helper/logging/Messaging.h>
#include <sofa/helper/AdvancedTimer.h>
#include <sofa/helper/system/FileRepository.h>
#include <sofa/helper/system/SetDirectory.h>
#include <sofa/helper/system/FileSystem.h>
#include <sofa/helper/Utils.h>

#include <algorithm>
#include <sofa/helper/system/FileRepository.h>
#include <sofa/simulation/SimulationLoop.h>
#include <map>


#include <sofa/core/objectmodel/KeypressedEvent.h>
#include <sofa/core/objectmodel/KeyreleasedEvent.h>
using namespace sofa;

namespace sofaglfw
Expand All @@ -59,7 +66,7 @@ SofaGLFWBaseGUI::SofaGLFWBaseGUI()
m_showSelectedObjectBoundingBox = false;
m_showSelectedObjectPositions = true;
m_showSelectedObjectSurfaces = true;
m_selectionColor = type::RGBAColor::orange();
m_selectionColor = type::RGBAColor(0.439, 0.588, 0.702, 1.);
}

SofaGLFWBaseGUI::~SofaGLFWBaseGUI()
Expand Down Expand Up @@ -92,7 +99,7 @@ bool SofaGLFWBaseGUI::init(int nbMSAASamples)
// max = 32 (MSAA with 32 samples)
glfwWindowHint(GLFW_SAMPLES, std::clamp(nbMSAASamples, 0, 32) );

m_glDrawTool = new sofa::gl::DrawToolGL();
m_glDrawTool = std::make_unique<sofa::gl::DrawToolGL>();
m_bGlfwIsInitialized = true;
return true;
}
Expand All @@ -113,7 +120,7 @@ void SofaGLFWBaseGUI::setSimulation(sofa::simulation::NodeSPtr groot, const std:
m_groot = groot;
m_filename = filename;

sofa::core::visual::VisualParams::defaultInstance()->drawTool() = m_glDrawTool;
sofa::core::visual::VisualParams::defaultInstance()->drawTool() = m_glDrawTool.get();
sofa::core::visual::VisualParams::defaultInstance()->setSupported(sofa::core::visual::API_OpenGL);
setScene(groot, filename.c_str());
load();
Expand Down Expand Up @@ -451,6 +458,8 @@ std::size_t SofaGLFWBaseGUI::runLoop(std::size_t targetNbIterations)
bool running = true;
std::size_t currentNbIterations = 0;
std::stringstream tmpStr;
std::vector<uint8_t> pixels;

while (!s_mapWindows.empty() && running)
{
SIMULATION_LOOP_SCOPE
Expand All @@ -476,10 +485,17 @@ std::size_t SofaGLFWBaseGUI::runLoop(std::size_t targetNbIterations)
m_guiEngine->startFrame(this);
m_guiEngine->endFrame();

glfwSwapBuffers(glfwWindow);

m_viewPortHeight = m_vparams->viewport()[3];
m_viewPortWidth = m_vparams->viewport()[2];

// Read framebuffer
if(this->groot->getAnimate() && this->m_isVideoRecording)
{
const auto [width, height] = this->m_guiEngine->getFrameBufferPixels(pixels);
m_videoRecorderFFMPEG.addFrame(pixels.data(), width, height);
}

glfwSwapBuffers(glfwWindow);
}
else
{
Expand Down Expand Up @@ -553,7 +569,7 @@ void SofaGLFWBaseGUI::initVisual()

void SofaGLFWBaseGUI::runStep()
{
if(simulationIsRunning())
if(simulationIsRunning() && m_guiEngine && m_groot)
{
sofa::helper::AdvancedTimer::begin("Animate");

Expand All @@ -571,7 +587,11 @@ void SofaGLFWBaseGUI::terminate()
if (!m_bGlfwIsInitialized)
return;

m_guiEngine->terminate();
if (m_guiEngine)
m_guiEngine->terminate();

if(m_isVideoRecording)
m_videoRecorderFFMPEG.finishVideo();

glfwTerminate();
}
Expand Down Expand Up @@ -742,6 +762,15 @@ void SofaGLFWBaseGUI::moveRayPickInteractor(int eventX, int eventY)
getPickHandler()->updateRay(position, direction);
}

void SofaGLFWBaseGUI::setMousePos(int xpos, int ypos) {
if(m_firstWindow)
{
glfwSetInputMode(m_firstWindow, GLFW_CURSOR, GLFW_CURSOR_HIDDEN); // Required on Wayland
glfwSetCursorPos(m_firstWindow, xpos, ypos);
glfwSetInputMode(m_firstWindow, GLFW_CURSOR, GLFW_CURSOR_NORMAL);
}
}

void SofaGLFWBaseGUI::window_pos_callback(GLFWwindow* window, int xpos, int ypos)
{
SofaGLFWBaseGUI* gui = static_cast<SofaGLFWBaseGUI*>(glfwGetWindowUserPointer(window));
Expand Down Expand Up @@ -1008,4 +1037,97 @@ bool SofaGLFWBaseGUI::centerWindow(GLFWwindow* window)
return true;
}

bool SofaGLFWBaseGUI::toggleVideoRecording()
{
if(m_isVideoRecording)
{
m_isVideoRecording = false;
m_videoRecorderFFMPEG.finishVideo();
}
else
{
// Initialize recorder with default parameters
const int width = std::max(1, m_viewPortWidth);
const int height = std::max(1, m_viewPortHeight);

if(initRecorder(width, height))
{
m_isVideoRecording = true;
}
else
{
msg_error("SofaGLFWBaseGUI") << "Failed to initialize recorder.";
return false;
}
}
return true;
}

bool SofaGLFWBaseGUI::initRecorder(int width,
int height,
unsigned int framerate,
unsigned int bitrate,
const std::string& codecExtension,
const std::string& codecName)
{
// Validate parameters
if (width <= 0 || height <= 0)
{
msg_error("SofaGLFWBaseGUI") << "Invalid video dimensions: " << width << "x" << height;
return false;
}

std::string ffmpeg_exec_path = "";
const std::string ffmpegIniFilePath = sofa::helper::Utils::getSofaPathTo("etc/SofaGLFW.ini");
std::map<std::string, std::string> iniFileValues = sofa::helper::Utils::readBasicIniFile(ffmpegIniFilePath);
if (iniFileValues.find("FFMPEG_EXEC_PATH") != iniFileValues.end())
{
// get absolute path of FFMPEG executable
ffmpeg_exec_path = sofa::helper::system::SetDirectory::GetRelativeFromProcess(iniFileValues["FFMPEG_EXEC_PATH"].c_str());
msg_info("SofaGLFWBaseGUI") << " The file " << ffmpegIniFilePath << " points to " << ffmpeg_exec_path << " for the ffmpeg executable.";
}
else
{
msg_warning("SofaGLFWBaseGUI") << " The file " << helper::Utils::getSofaPathPrefix() <<"/etc/SofaGLFW.ini either doesn't exist or doesn't contain the string FFMPEG_EXEC_PATH."
" The initialization of the FFMPEG video recorder will likely fail."
" To fix this, provide a valid path to the ffmpeg executable inside this file using the syntax \"FFMPEG_EXEC_PATH=/usr/bin/ffmpeg\".";
}

std::string screenshotPath = sofa::gui::common::BaseGUI::getScreenshotDirectoryPath();
m_videoFilePath = m_videoFilename.empty()? generateFilename("video", codecExtension): m_videoFilename;
m_videoFilePath = sofa::helper::system::FileSystem::append(screenshotPath, m_videoFilePath);
return m_videoRecorderFFMPEG.init(ffmpeg_exec_path, m_videoFilePath, width, height, framerate, bitrate, codecName);
}

bool SofaGLFWBaseGUI::isVideoRecording() const
{
return m_isVideoRecording;
}

std::string SofaGLFWBaseGUI::generateFilename(const std::string& prefix, const std::string &extension)
{
std::string filename = getSceneFileName();
// Add the date and time to the filename
auto now = std::chrono::system_clock::now();
auto localTime = std::chrono::system_clock::to_time_t(now);
std::stringstream ss;

if (!prefix.empty())
ss << prefix << "_";

if (!filename.empty())
{
std::filesystem::path path(filename);
ss << path.filename().replace_extension("").string() << "_";
}

ss << std::put_time(std::localtime(&localTime), "%F_%H-%M-%S");

if (!extension.empty())
ss << "." << extension;

filename = ss.str();
return filename;
}

} // namespace sofaglfw
Loading
Loading