Rendering large 2D panoramic image (13503×6935) using QtQuick3D on Raspberry Pi 5 – OpenGL ES max texture size limitation
-
Hello everybody!
On the Raspberry Pi 5, attempting to load the full image as a 3D texture results in rendering failure (black or distorted output) due to the hardware's 4096x4096 texture size limit. The same code works flawlessly on other system (in Windows) , where the larger texture size is supported.
I do not want to resize the image, as it leads to unacceptable quality loss for this use case (detailed panoramic view with interactive orbiting).
Environment
- Hardware: Raspberry Pi 5
- GPU / API: OpenGL ES
- Max texture size (glGetIntegerv): 4096
- OS: Raspberry Pi OS (Wayland)
- Python : 3.13.5
- PySide6 : 6.10.1
- Qt Modules: QtQuick, QtQuick3D
- Image format: PNG
- Image resolution : 13503 x 6935
What I've tried :
- By Tiling the Image with CustomeMaterial and shaders (Not Working )
- Cube Map Texture (Not Working)
Following are the relevant codes:
main.py :
import sys, os, re, ast, datetime, tempfile, requests, queue, threading, json # from pathlib import Path from pathlib import Path import pandas as pd import numpy as np # import sounddevice as sd from PySide6.QtCore import QUrl, QObject, Property, QAbstractListModel, Qt, QModelIndex, Slot, qVersion , QThread, Signal,QEvent from PySide6.QtGui import QGuiApplication from PySide6.QtQml import QQmlApplicationEngine from PySide6.QtMultimedia import QMediaPlayer, QAudioOutput from PySide6.QtMultimediaWidgets import QGraphicsVideoItem from PySide6.QtQuickControls2 import QQuickStyle # ============ PROJECT PATHS ============ project_dir = Path(__file__).parent.resolve() layouts_dir = project_dir / "assets" / "layouts" styles_dir = project_dir / "assets" / "styles" checker_dir = project_dir / "assets" / "hunspell" # ============ KEYBOARD CONFIGURATION ============ # CRITICAL: Set BEFORE creating QGuiApplication os.environ["QT_IM_MODULE"] = "qtvirtualkeyboard" # Point to our custom layout directory os.environ["QT_VIRTUALKEYBOARD_LAYOUT_PATH"] = str(layouts_dir) # Point to custom style directory os.environ["QT_VIRTUALKEYBOARD_STYLE_PATH"] = str(styles_dir) os.environ["QT_VIRTUALKEYBOARD_HUNSPELL_DICTIONARY_PATH"] = str(checker_dir) # Enable word prediction and suggestions os.environ["QT_VIRTUALKEYBOARD_HUNSPELL"] = "1" # Enable Hunspell dictionary # Number of word suggestions to show os.environ["QT_VIRTUALKEYBOARD_HUNSPELL_CHOICES"] = "6" # CRITICAL: Set the default input locale to match your dictionary os.environ["QT_VIRTUALKEYBOARD_DEFAULT_LOCALE"] = "en_US" # 8. Enable word prediction (very important!) os.environ["QT_VIRTUALKEYBOARD_WORD_PREDICTION"] = "1" # 9. Enable auto-capitalization os.environ["QT_VIRTUALKEYBOARD_AUTO_CAPITALIZATION"] = "1" # 10. Optional: Enable learning (keyboard learns new words) os.environ["QT_VIRTUALKEYBOARD_LEARN"] = "1" # Use our custom style (for bigger fonts) # os.environ["QT_VIRTUALKEYBOARD_STYLE"] = "myStyle" # Use our custom en_GB layout os.environ["QT_VIRTUALKEYBOARD_LAYOUTS"] = "en_US" # Disable default layouts (force use of custom only) os.environ["QT_VIRTUALKEYBOARD_DESKTOP_DISABLE"] = "0" os.environ['QT_IMAGEIO_MAXALLOC'] = "512" print("=" * 60) print(f" Qt Version: {qVersion()}") print("=" * 60) def on_about_to_quit(): pass if __name__ == "__main__": app = QGuiApplication(sys.argv) # Force a style that allows background customization QQuickStyle.setStyle("Fusion") # or "Material", "Basic" screens = app.screens() print(f"Screen Resolution is {screens}") if len(screens) > 1: screen_size = screens[1].size() device_pixel_ratio = screens[1].devicePixelRatio() else: screen_size = screens[0].size() device_pixel_ratio = screens[0].devicePixelRatio() print(f"Screen Resolution is {screen_size}, Scale Factor: {device_pixel_ratio}") engine = QQmlApplicationEngine() # Load main.qml engine.load(QUrl("main.qml")) app.aboutToQuit.connect(on_about_to_quit) if not engine.rootObjects(): sys.exit(-1) sys.exit(app.exec())main.qml :
import QtQuick import QtQuick.Controls import QtQuick.Window import QtMultimedia import QtQuick.Layouts import QtQuick3D import QtQuick3D.Helpers import QtQuick.Shapes Window { id: mainWindow visible: true visibility: Window.FullScreen color: "transparent" panaroma3D { id: subMenuPage1 mainViewImg: "./panaromicImg4.png" anchors.fill: parent visible: true } Component.onCompleted: { console.log("Screen resolution is:", width, "x", height) } }panaroma3D.qml :
import QtQuick import QtQuick.Controls import QtQuick.Layouts import QtQuick3D import QtQuick3D.Helpers import QtQuick.Shapes Item { id: control anchors.fill: parent z: 1 focus: true property alias caveEulerRotate: cameraRig.eulerRotation property alias caveFieldOfView: camera.fieldOfView required property string mainViewImg View3D { id: caveView3D anchors.fill: parent camera: camera environment: SceneEnvironment { backgroundMode: SceneEnvironment.Color clearColor: "black" } Node { id: sceneRoot // ─── Orbiting CameraRig ─── Node { id: cameraRig eulerRotation: Qt.vector3d(12, 0, 0) PerspectiveCamera { id: camera position: Qt.vector3d(0, 0, 0) // fixed distance for Viewing // position: Qt.vector3d(0, 0, 100) // fixed distance for Mapping Hotspot fieldOfView: 80 } } // ─── Central Sphere ─── Model { id: sphere objectName: "mainSphere" source: "#Sphere" scale: Qt.vector3d(-200, 200, 200) pickable: true materials: PrincipledMaterial { baseColorMap: Texture { source: control.mainViewImg } lighting: PrincipledMaterial.NoLighting cullMode: Material.NoCulling // cullMode: Material.FrontFaceCulling // fixed distance for Mapping Hotspot } } } // ─── Mouse Drag to Orbit ─── MouseArea { id: dragArea anchors.fill: caveView3D property real lastX: 0 property real lastY: 0 property bool isDragging: false onPressed: event => { lastX = event.x lastY = event.y isDragging = false } onPositionChanged: event => { let dx = event.x - lastX let dy = event.y - lastY if (Math.abs(dx) > 2 || Math.abs(dy) > 2) { isDragging = true // Horizontal orbit (yaw) let newYaw = cameraRig.eulerRotation.y + dx * 0.003 let newTemp = camera.position.z + dx * 0.003 newYaw = Math.max(125, Math.min(295, newYaw)) // actual working cameraRig.eulerRotation.y = newYaw // Vertical orbit (pitch) let newPitch = cameraRig.eulerRotation.x + dy * 0.003 newPitch = Math.max(12, Math.min(0, newPitch)) // clamp cameraRig.eulerRotation.x = newPitch lastX = event.x lastY = event.y } } onReleased: event => { if (!isDragging) { const result = caveView3D.pick(event.x, event.y) if (result.objectHit) { const name = result.objectHit.objectName if (name == "mainSphere") { console.log("👉 Clicked on the sphere surface at:", result.scenePosition) // handle sphere clicks here } } } } } } }