pythonpyqt5qt-signalsqt3dqpropertyanimation

How do I get the animation to work correctly in PyQt5 Qt3D?


I am trying to understand the animation operation in PyQt5 from the example in https://doc.qt.io/qt-5.10/qt3d-simple-cpp-example.html

I have translated the code into Python and am able to run it but there is no animation. I'm thinking I need to connect up the signals to an Update() method somewhere but I'm having troubel understanding how to do that.

Here is the PyQt5 (python 3) code:

######################################
#
# Copyright (C) 2014 Klaralvdalens Datakonsult AB (KDAB).
# Contact: https:#www.qt.io/licensing/
#
# This file is part of the Qt3D module of the Qt Toolkit.
#
# $QT_BEGIN_LICENSE:BSD$
# Commercial License Usage
# Licensees holding valid commercial Qt licenses may use this file in
# accordance with the commercial license agreement provided with the
# Software or, alternatively, in accordance with the terms contained in
# a written agreement between you and The Qt Company. For licensing terms
# and conditions see https:#www.qt.io/terms-conditions. For further
# information use the contact form at https:#www.qt.io/contact-us.
#
# BSD License Usage
# Alternatively, you may use this file under the terms of the BSD license
# as follows:
#
# "Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are
# met:
#   * Redistributions of source code must retain the above copyright
#     notice, this list of conditions and the following disclaimer.
#   * Redistributions in binary form must reproduce the above copyright
#     notice, this list of conditions and the following disclaimer in
#     the documentation and/or other materials provided with the
#     distribution.
#   * Neither the name of The Qt Company Ltd nor the names of its
#     contributors may be used to endorse or promote products derived
#     from this software without specific prior written permission.
#
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES LOSS OF USE,
# DATA, OR PROFITS OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE."
#
# $QT_END_LICENSE$
#
######################################/

# see https://doc.qt.io/qt-5.10/qt3d-examples.html

########################
# requirements.txt
# PyQt3D==5.10.1
# PyQt5==5.10.1
# pyqt5-tools==5.9.0.1.2
# QScintilla==2.10.4
# sip==4.19.8

from PyQt5.Qt3DCore import QEntity, QTransform
from PyQt5.Qt3DExtras import QTorusMesh, QPhongMaterial, \
    QSphereMesh, Qt3DWindow, QOrbitCameraController

from PyQt5.QtWidgets import QApplication
from PyQt5.QtGui import QVector3D, QQuaternion, QMatrix4x4
from PyQt5.QtCore import pyqtSlot, pyqtSignal, QPropertyAnimation

import sys


def fuzzyCompareDouble(p1, p2):
    """
    compares 2 double as points
    """
    return abs(p1 - p2) * 100000. <= min(abs(p1), abs(p2))

class OrbitTransformController(QTransform):
    targetChanged = pyqtSignal()
    angleChanged = pyqtSignal()
    radiusChanged = pyqtSignal()

    def __init__(self, parent):
        super().__init__(parent)
        self.m_target = QTransform()
        self.m_matrix = QMatrix4x4()
        self.m_radius = 1.0
        self.m_angle = 0.0
        #self.target.connect(self.targetChanged.emit)
        #self.angle.connect(self.angleChanged.emit)
        #self.radius.connect(self.radiusChanged.emit)

    def setTarget(self, target):
        if (self.m_target != target):
            self.m_target = target
        self.targetChanged.emit()

    def target(self, ): #  : # method of "", returning Qt3DCore.QTransform *OrbitTransformController (const)
        return self.m_target

    def setRadius(self, radius): #  : # method of "", returning void OrbitTransformController ()
        if not fuzzyCompareDouble(radius, self.m_radius):
            self.m_radius = radius
        self.radiusChanged.emit()

    def radius(self, ): #  : # method of "", returning float OrbitTransformController (const)
        return self.m_radius

    def setAngle(self, angle): #  : # method of "", returning void OrbitTransformController ()
        if not fuzzyCompareDouble(angle, self.m_angle):
            self.m_angle = angle
        print("setting angle %f" % angle)
        self.updateMatrix()
        self.angleChanged.emit()

    def angle(self, ): #  : # method of "", returning float OrbitTransformController (const)
        return self.m_angle

    def updateMatrix(self, ): #  : # method of "", returning void OrbitTransformController ()
        self.m_matrix.setToIdentity()
        self.m_matrix.rotate(self.m_angle, QVector3D(0.0, 1.0, 0.0))
        self.m_matrix.translate(self.m_radius, 0.0, 0.0)
        self.m_target.setMatrix(self.m_matrix)

def createScene():
    # Root entity
    rootEntity = QEntity()

    # Material
    material = QPhongMaterial(rootEntity)

    # Torus
    torusEntity = QEntity(rootEntity)
    # Qt3DExtras.QTorusMesh *
    torusMesh = QTorusMesh()
    torusMesh.setRadius(5)
    torusMesh.setMinorRadius(1)
    torusMesh.setRings(100)
    torusMesh.setSlices(20)

    #Qt3DCore.QTransform *
    torusTransform = QTransform()
    torusTransform.setScale3D(QVector3D(1.5, 1, 0.5))
    torusTransform.setRotation(QQuaternion.fromAxisAndAngle(QVector3D(1, 0, 0), 45.0))

    torusEntity.addComponent(torusMesh)
    torusEntity.addComponent(torusTransform)
    torusEntity.addComponent(material)

    # Sphere
    sphereEntity = QEntity(rootEntity)
    sphereMesh = QSphereMesh()
    sphereMesh.setRadius(3)

    # Qt3DCore.QTransform *
    sphereTransform = QTransform()
    #OrbitTransformController *
    controller = OrbitTransformController(sphereTransform)
    controller.setTarget(sphereTransform)
    controller.setRadius(20.0)
    # QPropertyAnimation *
    sphereRotateTransformAnimation = QPropertyAnimation(sphereTransform, b"angle")
    sphereRotateTransformAnimation.setTargetObject(controller)
    # sphereRotateTransformAnimation.setPropertyName("angle")  This is included when the object is created
    sphereRotateTransformAnimation.setStartValue(0)
    sphereRotateTransformAnimation.setEndValue(360)
    sphereRotateTransformAnimation.setDuration(10000)
    sphereRotateTransformAnimation.setLoopCount(-1)
    sphereRotateTransformAnimation.start()

    sphereEntity.addComponent(sphereMesh)
    sphereEntity.addComponent(sphereTransform)
    sphereEntity.addComponent(material)

    return rootEntity


if __name__ == "__main__":
    app = QApplication(sys.argv)
    view = Qt3DWindow()
    scene = createScene()

    # Camera
    camera = view.camera()
    camera.lens().setPerspectiveProjection(45.0, 16.0/9.0, 0.1, 1000.0)
    camera.setPosition(QVector3D(0, 0, 40.0))
    camera.setViewCenter(QVector3D(0, 0, 0))

    # For camera controls
    camController = QOrbitCameraController(scene)
    camController.setLinearSpeed( 50.0 )
    camController.setLookSpeed( 180.0 )
    camController.setCamera(camera)

    view.setRootEntity(scene)
    view.show()

    sys.exit(app.exec())

Is there something I'm missing? There is some O_OBJECT code that I dont knwo how to translate and I suspect this is where things are broken:

Q_OBJECT
Q_PROPERTY(Qt3DCore::QTransform* target READ target WRITE setTarget NOTIFY targetChanged)
Q_PROPERTY(float radius READ radius WRITE setRadius NOTIFY radiusChanged)
Q_PROPERTY(float angle READ angle WRITE setAngle NOTIFY angleChanged)

Any suggestions would be greatly appreciated. Here is a screenshot of the (unanimated) result.

Simple Sphere and Torus


Solution

  • You have to pyqtProperty:

    angle = pyqtProperty(float, fget=angle, fset=setAngle, notify=angleChanged)
    radius = pyqtProperty(float, fget=radius, fset=setRadius, notify=radiusChanged)
    target = pyqtProperty(QTransform, fget=target, fset=setTarget, notify=targetChanged)
    

    Complete Code:

    from PyQt5.Qt3DCore import QEntity, QTransform
    from PyQt5.Qt3DExtras import QTorusMesh, QPhongMaterial, \
        QSphereMesh, Qt3DWindow, QOrbitCameraController
    
    from PyQt5.QtWidgets import QApplication
    from PyQt5.QtGui import QVector3D, QQuaternion, QMatrix4x4
    from PyQt5.QtCore import pyqtSlot, pyqtSignal, QPropertyAnimation, pyqtProperty
    
    import sys
    
    
    def fuzzyCompareDouble(p1, p2):
        """
        compares 2 double as points
        """
        return abs(p1 - p2) * 100000. <= min(abs(p1), abs(p2))
    
    class OrbitTransformController(QTransform):
        targetChanged = pyqtSignal()
        angleChanged = pyqtSignal()
        radiusChanged = pyqtSignal()
    
        def __init__(self, parent):
            super().__init__(parent)
            self.m_target = QTransform()
            self.m_matrix = QMatrix4x4()
            self.m_radius = 1.0
            self.m_angle = 0.0
    
        def target(self):
            return self.m_target
    
        def setTarget(self, target):
            if self.m_target == target:
                return
            self.m_target = target
            self.targetChanged.emit()
    
    
        def setRadius(self, radius):
            if fuzzyCompareDouble(radius, self.m_radius):
                return
            self.m_radius = radius
            self.radiusChanged.emit()
    
        def radius(self, ): #  : # method of "", returning float OrbitTransformController (const)
            return self.m_radius
    
        def setAngle(self, angle): #  : # method of "", returning void OrbitTransformController ()
            if fuzzyCompareDouble(angle, self.m_angle):
                return
            self.m_angle = angle
            self.updateMatrix()
            self.angleChanged.emit()
    
        def angle(self): #  : # method of "", returning float OrbitTransformController (const)
            return self.m_angle
    
        def updateMatrix(self, ): #  : # method of "", returning void OrbitTransformController ()
            self.m_matrix.setToIdentity()
            self.m_matrix.rotate(self.m_angle, QVector3D(0.0, 1.0, 0.0))
            self.m_matrix.translate(self.m_radius, 0.0, 0.0)
            self.m_target.setMatrix(self.m_matrix)
    
        angle = pyqtProperty(float, fget=angle, fset=setAngle, notify=angleChanged)
        radius = pyqtProperty(float, fget=radius, fset=setRadius, notify=radiusChanged)
        target = pyqtProperty(float, fget=target, fset=setTarget, notify=angleChanged)
    
    def createScene():
        # Root entity
        rootEntity = QEntity()
    
        # Material
        material = QPhongMaterial(rootEntity)
    
        # Torus
        torusEntity = QEntity(rootEntity)
        # Qt3DExtras.QTorusMesh *
        torusMesh = QTorusMesh()
        torusMesh.setRadius(5)
        torusMesh.setMinorRadius(1)
        torusMesh.setRings(100)
        torusMesh.setSlices(20)
    
        #Qt3DCore.QTransform *
        torusTransform = QTransform()
        torusTransform.setScale3D(QVector3D(1.5, 1, 0.5))
        torusTransform.setRotation(QQuaternion.fromAxisAndAngle(QVector3D(1, 0, 0), 45.0))
    
        torusEntity.addComponent(torusMesh)
        torusEntity.addComponent(torusTransform)
        torusEntity.addComponent(material)
    
        # Sphere
        sphereEntity = QEntity(rootEntity)
        sphereMesh = QSphereMesh()
        sphereMesh.setRadius(3)
    
        # Qt3DCore.QTransform *
        sphereTransform = QTransform()
        #OrbitTransformController *
        controller = OrbitTransformController(sphereTransform)
        controller.setTarget(sphereTransform)
        controller.setRadius(20.0)
        # QPropertyAnimation *
        sphereRotateTransformAnimation = QPropertyAnimation(sphereTransform)
        sphereRotateTransformAnimation.setTargetObject(controller)
        sphereRotateTransformAnimation.setPropertyName(b"angle")
        sphereRotateTransformAnimation.setStartValue(0)
        sphereRotateTransformAnimation.setEndValue(360)
        sphereRotateTransformAnimation.setDuration(10000)
        sphereRotateTransformAnimation.setLoopCount(-1)
        sphereRotateTransformAnimation.start()
    
        sphereEntity.addComponent(sphereMesh)
        sphereEntity.addComponent(sphereTransform)
        sphereEntity.addComponent(material)
    
        return rootEntity
    
    
    if __name__ == "__main__":
        app = QApplication(sys.argv)
        view = Qt3DWindow()
        scene = createScene()
    
        # Camera
        camera = view.camera()
        camera.lens().setPerspectiveProjection(45.0, 16.0/9.0, 0.1, 1000.0)
        camera.setPosition(QVector3D(0, 0, 40.0))
        camera.setViewCenter(QVector3D(0, 0, 0))
    
        # For camera controls
        camController = QOrbitCameraController(scene)
        camController.setLinearSpeed( 50.0 )
        camController.setLookSpeed( 180.0 )
        camController.setCamera(camera)
    
        view.setRootEntity(scene)
        view.show()
    
        sys.exit(app.exec())