Created
August 13, 2023 22:00
-
-
Save kwahoo2/145dc9f7019c5d485a824225182d705f to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# This script creates a simple Qt Widget, renders it to a Coin3D texture (SoTexture2), and creates a cube (SoCube) with that texture. | |
# It also simulates (def simulate_picking(self)) a 3D ray that intersects with Coin3D render of the QWidget. If the point of intersection is on the "Hello World" button widget, then QMouseEvent (pressed and relesed) is triggered and button activation is animated. | |
from OpenGL import GL | |
from PySide2.QtWidgets import QOpenGLWidget, QApplication | |
from PySide2.QtCore import Qt, QTimer, Signal, Slot | |
from PySide2.QtGui import QOpenGLContext | |
from PySide2.QtGui import qGray, qRed, qGreen, qBlue, qAlpha | |
from pivy.coin import SoFrustumCamera | |
from pivy.coin import SoSeparator | |
from pivy.coin import SoSceneManager | |
from pivy.coin import SbViewportRegion | |
from pivy.coin import SbColor, SoBaseColor | |
from pivy.coin import SoTranslation | |
from pivy.coin import SoCube, SoSphere | |
from pivy.coin import SoDirectionalLight | |
from pivy.coin import SoTexture2, SoSFImage | |
from pivy.coin import SbVec2s, SbVec3f, SbVec4f | |
from pivy.coin import SoRayPickAction, SoLineSet, SoVertexProperty | |
import sys | |
import random | |
from PySide2.QtWidgets import QWidget, QVBoxLayout, QPushButton | |
from PySide2.QtGui import QPixmap, QPainter, QMouseEvent | |
from PySide2.QtCore import QPoint | |
class myOpenGLWidget(QOpenGLWidget): | |
simulate_click = Signal(SbVec4f) | |
gl_w_closed = Signal() | |
def __init__(self, parent=None): | |
QOpenGLWidget.__init__(self, parent) | |
self.pick_timer = QTimer() | |
self.pick_timer.timeout.connect(self.simulate_picking) | |
self.pick_timer.start(1000) | |
w = 600 | |
h = 600 | |
self.context = QOpenGLContext() | |
camera = SoFrustumCamera() | |
self.vp_reg = SbViewportRegion(w, h) | |
self.resize(w, h) | |
self.m_scenemanager = SoSceneManager() | |
self.m_scenemanager.setViewportRegion(self.vp_reg) | |
#self.m_scenemanager.setBackgroundColor(SbColor(0.0, 0.0, 0.8)) | |
self.rootScene = SoSeparator() | |
self.rootScene.ref() | |
camera.nearDistance.setValue(0.1) | |
camera.farDistance.setValue(1000) | |
camera.position.setValue(0, 0, -0.9) | |
camera.orientation.setValue(0.045, -0.045, -0.041, 0.997) | |
self.rootScene.addChild(camera) | |
light = SoDirectionalLight() | |
self.rootScene.addChild(light) | |
self.sep = SoSeparator() | |
self.pick_sep = SoSeparator() | |
self.rootScene.addChild(self.pick_sep) | |
self.rootScene.addChild(self.sep) | |
trans = SoTranslation() | |
trans.translation.setValue(0, 0.0, -2.5) | |
self.sep.addChild(trans) | |
# Make a cube | |
self.sep.addChild(SoCube()) | |
# Add a texture | |
self.textu = SoTexture2() | |
self.sep.insertChild(self.textu, 1) | |
self.m_scenemanager.setSceneGraph(self.rootScene) | |
self.col2 = SoBaseColor() # blue sphere to show intersection point | |
self.col2.rgb = SbColor(0, 0, 1) | |
self.pick_sep.addChild(self.col2) | |
self.line = SoLineSet() | |
self.pick_sep.addChild(self.line) | |
self.trans2 = SoTranslation() | |
self.pick_sep.addChild(self.trans2) | |
self.sph = SoSphere() | |
self.sph.radius.setValue(0.05) | |
self.pick_sep.addChild(self.sph) | |
def do_picking (self, start, dire): | |
# setup of the ray vector for picking | |
m_pick_action = SoRayPickAction(self.vp_reg) | |
m_pick_action.setRay(start, dire) # starting point and direction vector | |
print ("Ray: ", start.getValue(), dire.getValue()) | |
m_pick_action.apply(self.sep) | |
picked_point = m_pick_action.getPickedPoint() | |
if picked_point: | |
pnt_coords = picked_point.getPoint() # intersection SbVec3f | |
print ("Intersection point: ", pnt_coords.getValue()) | |
tex_coords = picked_point.getTextureCoords() # SbVec4f | |
print ("Texture intersection coords: ", tex_coords.getValue()) | |
self.col2.rgb = SbColor(0, 0, 1) | |
self.trans2.translation.setValue(pnt_coords) | |
vprop = SoVertexProperty() | |
vprop.vertex.set1Value(0, start) # Set first vertex to be 0, 0, 0 | |
vprop.vertex.set1Value(1, pnt_coords) # Set second vertex | |
self.line.vertexProperty = vprop | |
self.simulate_click.emit(tex_coords) | |
else: | |
self.col2.rgb = SbColor(1, 0, 0) # blue sphere if picked, red if not | |
self.update() | |
def simulate_picking(self): | |
# Creates a random 3D picking ray defined by two vectors: start point and direction | |
start = SbVec3f(0.0, 0.0, 0.0) | |
direction = SbVec3f(random.uniform(-0.5, 0.5), random.uniform(-0.2, 0.2) , -1.0) | |
self.do_picking(start, direction) | |
def initializeGL(self): | |
funcs = self.context.functions() | |
funcs.initializeOpenGLFunctions() | |
self.context.create() | |
def resizeGL(self, w, h): | |
self.vp_reg = SbViewportRegion(w, h) | |
self.m_scenemanager.setViewportRegion(self.vp_reg) | |
def paintGL(self): | |
GL.glEnable(GL.GL_LIGHTING) | |
GL.glEnable(GL.GL_CULL_FACE) | |
GL.glEnable(GL.GL_DEPTH_TEST) | |
self.m_scenemanager.render() | |
GL.glDisable(GL.GL_CULL_FACE) | |
GL.glDisable(GL.GL_DEPTH_TEST) | |
@Slot(SoSFImage) | |
def update_texture(self, img): | |
self.textu.image = img | |
self.update() | |
def closeEvent(self, event): | |
self.rootScene.unref() | |
self.gl_w_closed.emit() | |
event.accept() | |
class sourceWidget(QWidget): | |
widget_rend_soimage = Signal(SoSFImage) | |
def __init__(self, parent=None): | |
QWidget.__init__(self, parent) | |
# Create a QWidget and layout | |
layout = QVBoxLayout() | |
self.setLayout(layout) | |
self.button = QPushButton("Hello, World!") | |
layout.addWidget(self.button) | |
self.resize(300,300) | |
# circle showing where an event happened | |
self.circle_center = None | |
self.button.clicked.connect(self.button_click) | |
self.render_flag = False | |
def button_click(self): | |
print ("Button clicked") | |
self.button.blockSignals(True) | |
self.button.animateClick() # executes 100ms animated click, othewise event would be invisible for the user | |
QTimer.singleShot(200, self.unblock_button_signals) | |
def unblock_button_signals(self): | |
self.button.blockSignals(False) | |
def paintEvent(self, event): | |
if not self.render_flag: | |
QTimer.singleShot(0, self.render_widget) # avoid recursive repaint | |
def render_widget(self): | |
# Render the QWidget to an RGB image | |
self.render_flag = True | |
qimg = self.render_qwidget_to_rgb_image() | |
soimg = qimage_to_sosfimage(qimg) | |
self.widget_rend_soimage.emit(soimg) | |
self.render_flag = False | |
@Slot(SbVec4f) | |
def simulate_click(self, tex_coords): | |
s = self.size() | |
u_widget = int(s.width() * tex_coords[0]) | |
v_widget = int(s.height() * (1 - tex_coords[1])) | |
pos = QPoint(u_widget, v_widget) | |
glo_pos = self.mapToGlobal(pos) | |
target_widget = self.childAt(pos) | |
if target_widget: | |
pos_on_target = target_widget.mapFromGlobal(glo_pos) | |
press_event = QMouseEvent( | |
QMouseEvent.MouseButtonPress, | |
pos_on_target, | |
Qt.LeftButton, | |
Qt.LeftButton, | |
Qt.NoModifier, | |
) | |
QApplication.sendEvent(target_widget, press_event) | |
release_event = QMouseEvent( | |
QMouseEvent.MouseButtonRelease, | |
pos_on_target, | |
Qt.LeftButton, | |
Qt.LeftButton, | |
Qt.NoModifier, | |
) | |
QApplication.sendEvent(target_widget, release_event) | |
def render_qwidget_to_rgb_image(self): | |
# Create a QPixmap to render the widget | |
pixmap = QPixmap(self.size()) | |
# Create a QPainter and render the widget onto the pixmap | |
painter = QPainter(pixmap) | |
offset = QPoint(0, 0) | |
self.render(painter, offset) | |
painter.end() | |
# Convert the QPixmap to QImage | |
image = pixmap.toImage() | |
return image | |
def qimage_to_sosfimage(p): | |
# Convert QImage to SoSFImage | |
size = SbVec2s(p.width(), p.height()) | |
buffersize = p.sizeInBytes() | |
# print (buffersize, size[0], size[1]) | |
numcomponents = int(buffersize / (size[0] * size[1])) | |
img = SoSFImage() | |
width = size[0] | |
height = size[1] | |
byteList = bytearray() | |
for y in reversed(range(height)): # reversed because 0,0 is in left-top for Qt and left-bottom for GL | |
for x in range(width): | |
rgba = p.pixel(x, y) | |
if numcomponents <= 2: | |
byteList.append(qGray(rgba)) | |
if numcomponents == 2: | |
byteList.append(qAlpha(rgba)) | |
elif numcomponents <= 4: | |
byteList.append(qRed(rgba)) | |
byteList.append(qGreen(rgba)) | |
byteList.append(qBlue(rgba)) | |
if numcomponents == 4: | |
byteList.append(qAlpha(rgba)) | |
_bytes = bytes(byteList) | |
img.setValue(size, numcomponents, _bytes) | |
return img | |
if __name__ == '__main__': | |
QApplication.setAttribute(Qt.AA_UseDesktopOpenGL) | |
app = QApplication([]) | |
s_widget = sourceWidget() # QWidget | |
s_widget.setAttribute(Qt.WA_DontShowOnScreen) # QWidget is hidden, only the Coin3D window is visible | |
s_widget.show() | |
m_glwidget = myOpenGLWidget() # QOpenGLWidget with Coin3D scene | |
m_glwidget.show() | |
m_glwidget.simulate_click.connect(s_widget.simulate_click) | |
s_widget.widget_rend_soimage.connect(m_glwidget.update_texture) | |
m_glwidget.gl_w_closed.connect(s_widget.close) | |
sys.exit(app.exec_()) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
The "def qimage_to_sosfimage(p):"
is based on:
https://github.com/FreeCAD/FreeCAD/blob/master/src/Mod/Draft/draftutils/gui_utils.py