Search code examples
pythonopenglpyqtpyqtgraph

How to connect an oval or cylinder formed shape between two updating 3D points using pyqtgraph and OpenGL?


I would like to form a shape between two updating points in 3D space using pyqtgraph and OpenGL. For now, I have only found it possible to connect GLLinePlotItem and GLMeshItem with vertexes and flat faces between two points. However, I would like to have an oval or cylinder form connected between the points, but I cannot seem to find a way to use the integrated MeshData sphere and cylinder, without jumping into complicated mathematics, rotation matrices and trigonometry.

Is there a simpler way, similar to GLLinePlotItem or GLMeshItem?

Illustration of what I have right now, and what I would like to have instead:

enter image description here

Sample code:

from pyqtgraph.Qt import QtCore, QtGui
import pyqtgraph as pg
import pyqtgraph.opengl as gl
import numpy as np
import sys
from PyQt5.QtCore import QTimer
from PyQt5.QtWidgets import QMainWindow, QApplication
from random import randint

class MainWindow(QMainWindow):
    def __init__(self):
        super().__init__()

        w = gl.GLViewWidget()
        w.show()
        w.setCameraPosition(distance=15, azimuth=-90)

        self.timer = QTimer()
        self.timer.start(1000)
        self.timer.timeout.connect(self.start)

        g = gl.GLGridItem()
        g.scale(2, 2, 1)
        w.addItem(g)


        self.md = gl.MeshData.sphere(rows=10, cols=20)
        self.m1 = gl.GLMeshItem(meshdata=self.md,
                                smooth=True,
                                color=(1, 0, 0, 0.2),
                                shader="balloon",
                                glOptions="additive")
        w.addItem(self.m1)

        self.lineMesh = gl.GLLinePlotItem(width=1, antialias=False)
        w.addItem(self.lineMesh)


    def start(self):
        # coordinates

        point1 = np.array([randint(0,25), randint(0,25), 0])
        point2 = np.array([randint(0,25), randint(0,25), 20])

        line = np.array([point1, point2])
        self.lineMesh.setData(pos=line)

        length = (((point2[0] - point1[0]) ** 2 + (point2[1] - point1[1]) ** 2 + (
                point2[2] - point1[2]) ** 2) ** 0.5)*0.5

        center = (point1 + point2) / 2
        #radius = np.linalg.norm(point2 - point1) / 2


        self.md = gl.MeshData.sphere(rows=10, cols=20, radius=[1])
        self.m1.setMeshData(meshdata=self.md)

        self.m1.resetTransform()
        self.m1.scale(1, 1, length)
        self.m1.translate(*center)



if __name__ == '__main__':
    app = QApplication(sys.argv)
    ex = MainWindow()
    ex.show()
    sys.exit(app.exec_())

Solution

  • I don't think you can avoid having to do a little math here, but the trig isn't too bad:

            v = point2 - point1
            theta = np.arctan2(v[1], v[0])
            phi = np.arctan2(np.linalg.norm(v[:2]), v[2])
    
            tr = pg.Transform3D()
            tr.translate(*point1)
            tr.rotate(theta * 180 / np.pi, 0, 0, 1)
            tr.rotate(phi * 180 / np.pi, 0, 1, 0)
            tr.scale(1, 1, np.linalg.norm(v) / 2)
            tr.translate(0, 0, 1)
    
            self.m1.setTransform(tr)
    

    And if you prefer linear algebra rather than trigonometry, that's not too bad either, although a bit more verbose:

            # pick 4 points on the untransformed sphere
            a = np.array([
                [0., 0., -1.],
                [0., 0., 1.],
                [1., 0., -1.],
                [0., 1., -1.],
            ])
    
            # and 4 corresponding points on the transformed sphere
            v1 = np.cross(point1-point2, [0., 0., 1.])
            v2 = np.cross(point1-point2, v1)
            b = np.array([
                point1,
                point2,
                point1 + v1 / np.linalg.norm(v1),
                point1 + v2 / np.linalg.norm(v2),
            ])
    
            # solve the transform mapping from a to b
            tr = pg.solve3DTransform(a, b)
    
            # make this transform work in opengl's homogeneous coordinate system
            tr[3,3] = 1
    
            self.m1.setTransform(tr)