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_()