diff --git a/bindings/Modules/CMakeLists.txt b/bindings/Modules/CMakeLists.txt
index d1d0f74d0..d653248e4 100644
--- a/bindings/Modules/CMakeLists.txt
+++ b/bindings/Modules/CMakeLists.txt
@@ -2,6 +2,14 @@ project(Bindings.Modules)
set(MODULEBINDINGS_MODULE_LIST SofaBaseTopology SofaDeformable)
+find_package(Sofa.GL QUIET)
+if(Sofa.GL_FOUND)
+ list(APPEND MODULEBINDINGS_MODULE_LIST SofaGL)
+else()
+ message("Sofa.GL not detected. Python3 Bindings for Sofa.SofaGL will not be compiled.")
+endif()
+
+
foreach(modulebindings_module ${MODULEBINDINGS_MODULE_LIST})
add_subdirectory(src/SofaPython3/${modulebindings_module})
endforeach()
@@ -19,4 +27,4 @@ install(TARGETS ${PROJECT_NAME} EXPORT BindingsTargets)
if(SP3_BUILD_TEST)
add_subdirectory(tests)
-endif()
\ No newline at end of file
+endif()
diff --git a/bindings/Modules/src/SofaPython3/SofaGL/Binding_DrawToolGL.cpp b/bindings/Modules/src/SofaPython3/SofaGL/Binding_DrawToolGL.cpp
new file mode 100644
index 000000000..1348362ae
--- /dev/null
+++ b/bindings/Modules/src/SofaPython3/SofaGL/Binding_DrawToolGL.cpp
@@ -0,0 +1,55 @@
+/******************************************************************************
+* SofaPython3 plugin *
+* (c) 2021 CNRS, University of Lille, INRIA *
+* *
+* This program is free software; you can redistribute it and/or modify it *
+* under the terms of the GNU Lesser General Public License as published by *
+* the Free Software Foundation; either version 2.1 of the License, or (at *
+* your option) any later version. *
+* *
+* This program is distributed in the hope that it will be useful, but WITHOUT *
+* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or *
+* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License *
+* for more details. *
+* *
+* You should have received a copy of the GNU Lesser General Public License *
+* along with this program. If not, see . *
+*******************************************************************************
+* Contact information: contact@sofa-framework.org *
+******************************************************************************/
+
+#include
+
+#include
+using sofa::simulation::Node;
+
+#include
+
+#include
+#include
+
+#include
+#include
+
+
+namespace sofapython3 {
+
+using DrawToolGL = sofa::gl::DrawToolGL;
+
+
+namespace py { using namespace pybind11; }
+
+void moduleAddDrawToolGL(pybind11::module& m) {
+ m.def("draw", [](Node* node){
+ auto* vparam = sofa::core::visual::VisualParams::defaultInstance();
+ vparam->drawTool() = new sofa::gl::DrawToolGL();
+ vparam->setSupported(sofa::core::visual::API_OpenGL);
+ sofa::simulation::getSimulation()->draw(vparam, node);
+ }, doc::SofaGL::draw);
+
+ m.def("glewInit", [](){
+ glewInit();
+ }, doc::SofaGL::glewInit);
+}
+
+} // namespace sofapython3
diff --git a/bindings/Modules/src/SofaPython3/SofaGL/Binding_DrawToolGL.h b/bindings/Modules/src/SofaPython3/SofaGL/Binding_DrawToolGL.h
new file mode 100644
index 000000000..f41258eff
--- /dev/null
+++ b/bindings/Modules/src/SofaPython3/SofaGL/Binding_DrawToolGL.h
@@ -0,0 +1,29 @@
+/******************************************************************************
+* SOFA, Simulation Open-Framework Architecture *
+* (c) 2021 INRIA, USTL, UJF, CNRS, MGH *
+* *
+* This program is free software; you can redistribute it and/or modify it *
+* under the terms of the GNU Lesser General Public License as published by *
+* the Free Software Foundation; either version 2.1 of the License, or (at *
+* your option) any later version. *
+* *
+* This program is distributed in the hope that it will be useful, but WITHOUT *
+* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or *
+* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License *
+* for more details. *
+* *
+* You should have received a copy of the GNU Lesser General Public License *
+* along with this program. If not, see . *
+*******************************************************************************
+* Contact information: contact@sofa-framework.org *
+******************************************************************************/
+
+#pragma once
+
+#include
+
+namespace sofapython3 {
+
+void moduleAddDrawToolGL(pybind11::module& m);
+
+} // namespace sofapython3
diff --git a/bindings/Modules/src/SofaPython3/SofaGL/Binding_DrawToolGL_doc.h b/bindings/Modules/src/SofaPython3/SofaGL/Binding_DrawToolGL_doc.h
new file mode 100644
index 000000000..085024f68
--- /dev/null
+++ b/bindings/Modules/src/SofaPython3/SofaGL/Binding_DrawToolGL_doc.h
@@ -0,0 +1,36 @@
+/******************************************************************************
+* SOFA, Simulation Open-Framework Architecture *
+* (c) 2021 INRIA, USTL, UJF, CNRS, MGH *
+* *
+* This program is free software; you can redistribute it and/or modify it *
+* under the terms of the GNU Lesser General Public License as published by *
+* the Free Software Foundation; either version 2.1 of the License, or (at *
+* your option) any later version. *
+* *
+* This program is distributed in the hope that it will be useful, but WITHOUT *
+* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or *
+* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License *
+* for more details. *
+* *
+* You should have received a copy of the GNU Lesser General Public License *
+* along with this program. If not, see . *
+*******************************************************************************
+* Contact information: contact@sofa-framework.org *
+******************************************************************************/
+
+#pragma once
+
+namespace sofapython3::doc::SofaGL {
+static auto draw =
+R"(
+Draw the openGL visual loop from the provided Node.
+:param node: Node to start visualize from
+:type node: Sofa::Simulation::Node
+)";
+
+static auto glewInit =
+R"(
+Initialize GLEW. This should be called once before drawing.
+)";
+
+} // sofapython3::doc::SofaGL
diff --git a/bindings/Modules/src/SofaPython3/SofaGL/CMakeLists.txt b/bindings/Modules/src/SofaPython3/SofaGL/CMakeLists.txt
new file mode 100644
index 000000000..2e8ad764c
--- /dev/null
+++ b/bindings/Modules/src/SofaPython3/SofaGL/CMakeLists.txt
@@ -0,0 +1,28 @@
+project(Bindings.Modules.SofaGL)
+
+set(SOURCE_FILES
+ ${CMAKE_CURRENT_SOURCE_DIR}/Module_SofaGL.cpp
+ ${CMAKE_CURRENT_SOURCE_DIR}/Binding_DrawToolGL.cpp
+)
+
+set(HEADER_FILES
+ ${CMAKE_CURRENT_SOURCE_DIR}/Binding_DrawToolGL.h
+ ${CMAKE_CURRENT_SOURCE_DIR}/Binding_DrawToolGL_doc.h
+)
+
+if (NOT TARGET SofaPython3::Plugin)
+ find_package(SofaPython3 REQUIRED)
+endif()
+
+find_package(Sofa.GL REQUIRED)
+
+SP3_add_python_module(
+ TARGET ${PROJECT_NAME}
+ PACKAGE Bindings
+ MODULE SofaGL
+ DESTINATION Sofa
+ SOURCES ${SOURCE_FILES}
+ HEADERS ${HEADER_FILES}
+ DEPENDS Sofa.GL SofaPython3::Plugin SofaPython3::Bindings.Sofa.Core
+
+)
diff --git a/bindings/Modules/src/SofaPython3/SofaGL/Module_SofaGL.cpp b/bindings/Modules/src/SofaPython3/SofaGL/Module_SofaGL.cpp
new file mode 100644
index 000000000..426e93beb
--- /dev/null
+++ b/bindings/Modules/src/SofaPython3/SofaGL/Module_SofaGL.cpp
@@ -0,0 +1,35 @@
+/******************************************************************************
+* SOFA, Simulation Open-Framework Architecture *
+* (c) 2021 INRIA, USTL, UJF, CNRS, MGH *
+* *
+* This program is free software; you can redistribute it and/or modify it *
+* under the terms of the GNU Lesser General Public License as published by *
+* the Free Software Foundation; either version 2.1 of the License, or (at *
+* your option) any later version. *
+* *
+* This program is distributed in the hope that it will be useful, but WITHOUT *
+* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or *
+* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License *
+* for more details. *
+* *
+* You should have received a copy of the GNU Lesser General Public License *
+* along with this program. If not, see . *
+*******************************************************************************
+* Contact information: contact@sofa-framework.org *
+******************************************************************************/
+
+#include
+#include
+
+
+namespace py { using namespace pybind11; }
+
+namespace sofapython3
+{
+
+PYBIND11_MODULE(SofaGL, m)
+{
+ moduleAddDrawToolGL(m);
+}
+
+} // namespace sofapython3
diff --git a/examples/additional-examples/pygame_example.py b/examples/additional-examples/pygame_example.py
new file mode 100644
index 000000000..d353713de
--- /dev/null
+++ b/examples/additional-examples/pygame_example.py
@@ -0,0 +1,135 @@
+import Sofa
+import Sofa.SofaGL
+import SofaRuntime
+import Sofa.Simulation as sim
+import os
+import time
+sofa_directory = os.environ['SOFA_ROOT']
+import pygame
+from OpenGL.GL import *
+from OpenGL.GLU import *
+
+
+pygame.display.init()
+display_size = (800, 600)
+pygame.display.set_mode(display_size, pygame.DOUBLEBUF | pygame.OPENGL)
+
+glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT)
+glEnable(GL_LIGHTING)
+glEnable(GL_DEPTH_TEST)
+Sofa.SofaGL.glewInit()
+glMatrixMode(GL_PROJECTION)
+glLoadIdentity()
+gluPerspective(45, (display_size[0] / display_size[1]), 0.1, 50.0)
+
+glMatrixMode(GL_MODELVIEW)
+glLoadIdentity()
+
+def simple_render(rootNode):
+ """
+ Get the OpenGL Context to render an image (snapshot) of the simulation state
+ """
+ glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT)
+ glEnable(GL_LIGHTING)
+ glEnable(GL_DEPTH_TEST)
+ glMatrixMode(GL_PROJECTION)
+ glLoadIdentity()
+ gluPerspective(45, (display_size[0] / display_size[1]), 0.1, 50.0)
+ glMatrixMode(GL_MODELVIEW)
+ glLoadIdentity()
+
+ cameraMVM = rootNode.visuals.camera.getOpenGLModelViewMatrix()
+ glMultMatrixd(cameraMVM)
+
+ Sofa.SofaGL.draw(rootNode.visuals)
+
+ pygame.display.get_surface().fill((0,0,0))
+ pygame.display.flip()
+
+
+class scene_interface:
+ """Scene_interface provides step and reset methods"""
+
+ def __init__(self, max_steps=1000):
+ # max_steps, how long the simulator should run. Total length: dt*max_steps
+ self.max_steps = max_steps
+
+ # the current step in the simulation
+ self.current_step = 0
+ # Register all the common component in the factory.
+ SofaRuntime.PluginRepository.addFirstPath(os.path.join(sofa_directory, 'plugins'))
+ SofaRuntime.importPlugin('SofaOpenglVisual')
+ SofaRuntime.importPlugin("SofaComponentAll")
+ SofaRuntime.importPlugin("SofaGeneralLoader")
+ SofaRuntime.importPlugin("SofaImplicitOdeSolver")
+ SofaRuntime.importPlugin("SofaLoader")
+ SofaRuntime.importPlugin("SofaSimpleFem")
+ SofaRuntime.importPlugin("SofaBoundaryCondition")
+ SofaRuntime.importPlugin("SofaMiscForceField")
+
+ self.root = Sofa.Core.Node("myroot")
+
+ ### create some objects to observe
+ self.place_objects_in_scene(self.root)
+
+ # place light and a camera
+ self.visuals = self.root.addChild('visuals')
+ self.visuals.addObject("LightManager")
+ self.visuals.addObject("SpotLight", position=[0,10,0], direction=[0,-1,0])
+ self.visuals.addObject("InteractiveCamera", name="camera", position=[0,10, 0],
+ lookAt=[0,0,0], distance=37,
+ fieldOfView=45, zNear=0.63, zFar=55.69)
+
+ # start the simulator
+ Sofa.Simulation.init(self.root)
+
+ def place_objects_in_scene(self, root):
+ ### these are just some things that stay still and move around
+ # so you know the animation is actually happening
+ root.gravity = [0, -1., 0]
+ root.addObject("VisualStyle", displayFlags="showBehaviorModels showAll")
+ root.addObject("MeshGmshLoader", name="meshLoaderCoarse",
+ filename="mesh/liver.msh")
+ root.addObject("MeshObjLoader", name="meshLoaderFine",
+ filename="mesh/liver-smooth.obj")
+
+ root.addObject("EulerImplicitSolver")
+ root.addObject("CGLinearSolver", iterations="200",
+ tolerance="1e-09", threshold="1e-09")
+
+ liver = root.addChild("liver")
+
+ liver.addObject("TetrahedronSetTopologyContainer",
+ name="topo", src="@../meshLoaderCoarse" )
+ liver.addObject("TetrahedronSetGeometryAlgorithms",
+ template="Vec3d", name="GeomAlgo")
+ liver.addObject("MechanicalObject",
+ template="Vec3d",
+ name="MechanicalModel", showObject="1", showObjectScale="3")
+
+ liver.addObject("TetrahedronFEMForceField", name="fem", youngModulus="1000",
+ poissonRatio="0.4", method="large")
+
+ liver.addObject("MeshMatrixMass", massDensity="1")
+ liver.addObject("FixedConstraint", indices="2 3 50")
+
+ def step(self):
+ # this steps the simulation
+ Sofa.Simulation.animate(self.root, self.root.getDt())
+ self.visuals.camera.position = self.visuals.camera.position + [-0.002, 0, 0]
+ simple_render(self.root)
+
+ # just to keep track of where we are
+ self.current_step += 1
+
+ # return true if done
+ return self.current_step >= self.max_steps
+
+
+if __name__ == '__main__':
+ a = scene_interface()
+ done = False
+ while not done:
+ factor = a.current_step
+ done = a.step()
+ time.sleep(a.root.getDt())
diff --git a/examples/additional-examples/pyqt_example.py b/examples/additional-examples/pyqt_example.py
new file mode 100644
index 000000000..5c6d2f2bb
--- /dev/null
+++ b/examples/additional-examples/pyqt_example.py
@@ -0,0 +1,156 @@
+from qtpy.QtCore import *
+from qtpy.QtWidgets import *
+from qtpy.QtOpenGL import *
+import Sofa.SofaGL as SGL
+import Sofa
+import SofaRuntime
+import Sofa.Simulation as sim
+import os
+sofa_directory = os.environ['SOFA_ROOT']
+from OpenGL.GL import *
+from OpenGL.GLU import *
+import numpy as np
+from PIL import Image
+
+
+"""
+With something like this setup, we can use Sofa with our own GUI and not have to give over control of the main thread.
+Simple pyqt signals can be added to manually rotate the view with the mouse (could be a tedious task to get it to
+be intuitive).
+"""
+
+class MainWindow(QWidget):
+ def __init__(self):
+ super(MainWindow, self).__init__()
+ self.sofa_sim = SofaSim()
+ self.sofa_sim.init_sim()
+ self.sofa_view = glSofaWidget(self.sofa_sim.visuals)
+ mainLayout = QHBoxLayout()
+ mainLayout.addWidget(self.sofa_view)
+ self.setLayout(mainLayout)
+ self.simulation_timer = QTimer()
+ self.simulation_timer.timeout.connect(self.step_sim)
+ self.simulation_timer.setInterval(self.sofa_sim.root.getDt())
+ self.simulation_timer.start()
+
+ def step_sim(self):
+ self.sofa_sim.step_sim()
+ self.sofa_view.update()
+
+ def keyPressEvent(self, QKeyEvent):
+ if QKeyEvent.key() == Qt.Key_Space:
+ self.sofa_view.get_depth_image()
+
+
+class glSofaWidget(QGLWidget):
+ def __init__(self, sofa_visuals_node):
+ QGLWidget.__init__(self)
+ self.visuals_node = sofa_visuals_node
+ self.setMinimumSize(800, 600)
+ self.z_far = sofa_visuals_node.camera.findData('zFar').value
+ self.z_near = sofa_visuals_node.camera.findData('zNear').value
+
+ def initializeGL(self):
+ glViewport(0,0, self.width(), self.height())
+ glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT)
+ glEnable(GL_LIGHTING)
+ glEnable(GL_DEPTH_TEST)
+ glDepthFunc(GL_LESS)
+ SGL.glewInit()
+ glMatrixMode(GL_PROJECTION)
+ glLoadIdentity()
+ gluPerspective(45, (self.width() / self.height()), 0.1, 50.0)
+ glMatrixMode(GL_MODELVIEW)
+ glLoadIdentity()
+
+ def paintGL(self):
+ glViewport(0,0, self.width(), self.height())
+ glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT)
+ glMatrixMode(GL_PROJECTION)
+ glLoadIdentity()
+ gluPerspective(45, (self.width()/ self.height()), 0.1, 50.0)
+ glMatrixMode(GL_MODELVIEW)
+ glLoadIdentity()
+
+ cameraMVM = self.visuals_node.camera.getOpenGLModelViewMatrix()
+ glMultMatrixd(cameraMVM)
+
+ SGL.draw(self.visuals_node)
+
+ def get_depth_image(self):
+ _, _, width, height = glGetIntegerv(GL_VIEWPORT)
+ buff = glReadPixels(0, 0, width, height, GL_DEPTH_COMPONENT, GL_FLOAT)
+ image = np.frombuffer(buff, dtype=np.float32)
+ image = image.reshape(height, width)
+ image = np.flipud(image) #<-- image is now a numpy array you can use
+ depth_image = -self.z_far*self.z_near/(self.z_far + image*(self.z_near-self.z_far))
+ depth_image = (depth_image - depth_image.min()) / (depth_image.max() - depth_image.min())
+ depth_image = depth_image * 255
+
+ img2 = Image.fromarray(depth_image.astype(np.uint8), 'L')
+ img2.show()
+
+
+class SofaSim():
+ def __init__(self):
+ # Register all the common component in the factory.
+ SofaRuntime.PluginRepository.addFirstPath(os.path.join(sofa_directory, 'plugins'))
+ SofaRuntime.importPlugin('SofaOpenglVisual')
+ SofaRuntime.importPlugin("SofaComponentAll")
+ SofaRuntime.importPlugin("SofaGeneralLoader")
+ SofaRuntime.importPlugin("SofaImplicitOdeSolver")
+ SofaRuntime.importPlugin("SofaLoader")
+ SofaRuntime.importPlugin("SofaSimpleFem")
+ SofaRuntime.importPlugin("SofaBoundaryCondition")
+ SofaRuntime.importPlugin("SofaMiscForceField")
+ self.root = Sofa.Core.Node("Root")
+ root = self.root
+ root.gravity = [0, -1., 0]
+ root.addObject("VisualStyle", displayFlags="showBehaviorModels showAll")
+ root.addObject("MeshGmshLoader", name="meshLoaderCoarse",
+ filename="mesh/liver.msh")
+ root.addObject("MeshObjLoader", name="meshLoaderFine",
+ filename="mesh/liver-smooth.obj")
+
+ root.addObject("EulerImplicitSolver")
+ root.addObject("CGLinearSolver", iterations="200",
+ tolerance="1e-09", threshold="1e-09")
+
+ liver = root.addChild("liver")
+
+ liver.addObject("TetrahedronSetTopologyContainer",
+ name="topo", src="@../meshLoaderCoarse")
+ liver.addObject("TetrahedronSetGeometryAlgorithms",
+ template="Vec3d", name="GeomAlgo")
+ liver.addObject("MechanicalObject",
+ template="Vec3d",
+ name="MechanicalModel", showObject="1", showObjectScale="3")
+
+ liver.addObject("TetrahedronFEMForceField", name="fem", youngModulus="1000",
+ poissonRatio="0.4", method="large")
+
+ liver.addObject("MeshMatrixMass", massDensity="1")
+ liver.addObject("FixedConstraint", indices="2 3 50")
+
+ # place light and a camera
+ self.visuals = root.addChild('visuals')
+ self.visuals.addObject("LightManager")
+ self.visuals.addObject("SpotLight", position=[0, 10, 0], direction=[0, -1, 0])
+ self.visuals.addObject("InteractiveCamera", name="camera", position=[0, 10, 0],
+ lookAt=[0, 0, 0], distance=37,
+ fieldOfView=45, zNear=0.63, zFar=55.69)
+
+ def init_sim(self):
+ # start the simulator
+ Sofa.Simulation.init(self.root)
+
+ def step_sim(self):
+ self.visuals.camera.position = self.visuals.camera.position + [-0.0002, 0, 0]
+ Sofa.Simulation.animate(self.root, self.root.getDt())
+
+
+if __name__ == '__main__':
+ app = QApplication(['Yo'])
+ window = MainWindow()
+ window.show()
+ app.exec_()