MAX
MAX

Reputation: 23

Render the view of a Qt3DWindow to an image

I have a Qt3DWindow inside a QWidget container, with a QSphere added to it (and material, lighting, camera). This works all well when I launch it and I can see the sphere. I now want to "capture" / "render" this view and save it as an image. I have spent the whole day trying to figure out how to achieve this seemingly very simple task, but everything has failed thus far. I have tried QRenderCapture(), grab(), and QOffscreenSurface() as recommended by some common AI tools, to no avail. Here is the working code of the window and viewer.

import sys
from PyQt6.QtWidgets import QApplication, QHBoxLayout, QWidget, QPushButton, 
from PyQt6.QtWidgets import QMainWindow, QWidget
from PyQt6.QtGui import QIcon, QPixmap, QPainter, QImage, QMatrix4x4, QQuaternion, QVector3D, QColor, QGuiApplication, QPageLayout
from PyQt6.QtCore import QTimer
from PyQt6.Qt3DCore import QEntity, QTransform
from PyQt6 import Qt3DRender
from PyQt6.Qt3DExtras import QForwardRenderer, QPhongMaterial, Qt3DWindow, QOrbitCameraController, QDiffuseSpecularMaterial, QTextureMaterial, QNormalDiffuseMapMaterial, QDiffuseMapMaterial 
from PyQt6.Qt3DExtras import QSphereMesh
from PyQt6.Qt3DRender import QRenderCapture, QRenderCaptureReply
from PyQt6.QtCore import QTimer

class MainWindow(QMainWindow):
    def __init__(self):
        super(MainWindow, self).__init__()
        self.view = Qt3DWindow()
        self.container = QWidget.createWindowContainer(self.view)
        self.setCentralWidget(self.container)

        # Create & set layouts
        main_layout = QHBoxLayout()
        main_layout.addWidget(self.container)
        
        main_widget = QWidget()
        main_widget.setLayout(main_layout)
        self.setCentralWidget(main_widget)
       
        # Root entity
        self.rootEntity = QEntity()

        # Load mesh
        sphere_mesh = QSphereMesh()
        sphere_mesh.setRadius(20)

        mesh_entity = QEntity(self.rootEntity)
        mesh_entity.addComponent(sphere_mesh)
        
        # Load/create textures
        diffuse_material = QPhongMaterial(self.rootEntity)
        diffuse_material.setDiffuse(QColor.fromRgbF(0.1, 0.9, 0.7, 0.6))
        mesh_entity.addComponent(diffuse_material)

        # Add material components to entity
        mesh_entity.addComponent(diffuse_material)

        # Camera
        self.camera = self.view.camera()
        self.camera.lens().setPerspectiveProjection(60.0, 16.0 / 9.0, 1, 1000.0)
        self.camera.setPosition(QVector3D(0.0, 0, 120.0))
        self.camera.setViewCenter(QVector3D(0.0, 0.0, 0.0))
        
        # camera controls
        self.camController = QOrbitCameraController(self.rootEntity)
        self.camController.setLinearSpeed(200.0)
        self.camController.setLookSpeed(280.0)
        self.camController.setCamera(self.camera)
        
        # Light
        light_entity = QEntity(self.rootEntity)
        light = Qt3DRender.QPointLight(light_entity)
        light.setConstantAttenuation(0)
        light_entity.addComponent(light)
        light_transform = QTransform()
        light_transform.setTranslation(QVector3D(350, 100, 200))
        light_entity.addComponent(light_transform)
        
        self.view.setRootEntity(self.rootEntity)

        self.capture_btn = QPushButton('Capture', self)
        self.capture_btn.clicked.connect(self.capture_image)
        main_layout.addWidget(self.capture_btn)

    def capture_image(self):
        pass

if __name__ == '__main__':
    app = QApplication(sys.argv)
    window = MainWindow()
    window.setGeometry(100, 100, 800, 600)
    window.setWindowTitle("3D Object Viewer")
    window.show()
    sys.exit(app.exec())

Upvotes: 0

Views: 40

Answers (1)

MAX
MAX

Reputation: 23

Solved. I managed to get it working. Trick was to link the FrameGraph to the QRenderCapture instance. Here is the code added to init()

self.render_capture = QRenderCapture()
self.view.activeFrameGraph().setParent(self.render_capture)
self.view.setActiveFrameGraph(self.render_capture)

The other gotcha is to wait for the rendering to complete. Here is how I implemented it.

def capture_image(self):
   self.reply = self.render_capture.requestCapture()
   loop = QEventLoop()
   self.reply.completed.connect(loop.quit)
   QTimer.singleShot(200, loop.quit)  # Timeout after 1 second
   loop.exec()
        
   image = self.reply.image()
   image.save("capture_output.jpg", "JPG")

I just can't believe how poorly documented this Qt3D library is...

Upvotes: 0

Related Questions