In this article, we will build a simple 3D modeling and animation app using Python. We will use PyQt for the GUI and PyOpenGL to render and rotate a 3D cube. This beginner-friendly guide walks you through the entire setup step-by-step.
Installation
Install the required libraries using pip:
pip install PyOpenGL PyQt5
- PyOpenGL: Python bindings for OpenGL, used to draw and render 3D geometry.
- PyQt5: GUI toolkit used to create the application window, layouts and controls.
Now let’s look at the step-by-step implementation of this app.
Step by step Implementation
Step 1: Import the Required Libraries
Before starting the app, we import everything needed for GUI creation, OpenGL rendering, and math calculations.
from PyQt5.QtWidgets import QMainWindow, QApplication, QVBoxLayout, QWidget, QPushButton
from PyQt5.QtOpenGL import QGLWidget
from OpenGL.GL import *
import math
import sys
Explanation:
- QMainWindow, QApplication, QVBoxLayout, QWidget, QPushButton: provide the main window, event loop, vertical layout, widget container, and a button for the GUI.
- QGLWidget: supplies an OpenGL surface inside a Qt widget where we render OpenGL content.
- from OpenGL.GL import *: brings in OpenGL drawing and state functions such as glBegin, glVertex3f, glRotatef, etc.
- math: used here for math helpers (e.g., math.tan, math.radians) when setting the projection.
- sys: used to read command-line args and to exit the application cleanly.
Step 2: Create the Main Application Window
Here we create the window, add the OpenGL widget and a button for future actions.
class MainWindow(QMainWindow):
def __init__(self):
super().__init__()
self.initUI()
def initUI(self):
self.setGeometry(100, 100, 800, 600)
self.setWindowTitle('3D Modeling & Animation App')
layout = QVBoxLayout()
central_widget = QWidget(self)
central_widget.setLayout(layout)
self.setCentralWidget(central_widget)
self.glWidget = GLWidget()
layout.addWidget(self.glWidget)
btn_create_object = QPushButton('Create Object', self)
btn_create_object.clicked.connect(self.create_object)
layout.addWidget(btn_create_object)
self.show()
def create_object(self):
print('Creating 3D object...')
Explanation
- setGeometry(...) sets the window position and size
- setWindowTitle(...) sets the title, the layout and central widget arrange child widgets
- GLWidget() is added as the OpenGL drawing area
- The button is wired to create_object() for later use.
Step 3: Initialize OpenGL Settings
Now we prepare the OpenGL environment by setting a background color and enabling depth testing so the cube looks properly 3D.
def initializeGL(self):
glClearColor(0.0, 0.0, 0.0, 1.0)
glEnable(GL_DEPTH_TEST)
Explanation:
- glClearColor(...) sets the background color to black
- glEnable(GL_DEPTH_TEST) enables depth testing so nearer fragments hide farther ones.
Step 4: Adjust Projection When Window Resizes
Whenever the window size changes, we update the camera view so the cube stays proportionate.
def resizeGL(self, width, height):
glViewport(0, 0, width, height)
glMatrixMode(GL_PROJECTION)
glLoadIdentity()
aspect_ratio = width / height
fov_degrees = 45.0
near_clip = 0.1
far_clip = 100.0
top = near_clip * math.tan(math.radians(fov_degrees / 2.0))
bottom = -top
left = bottom * aspect_ratio
right = top * aspect_ratio
glFrustum(left, right, bottom, top, near_clip, far_clip)
glMatrixMode(GL_MODELVIEW)
Explanation:
- glViewport(...) sets the drawable region
- glFrustum(...) constructs a perspective projection based on field-of-view and aspect ratio
- switching back to GL_MODELVIEW prepares OpenGL for drawing objects.
Step 5: Draw and Animate a Rotating 3D Cube
This method clears the frame, sets camera transforms, draws six faces as quads, and increments the rotation for animation.
def paintGL(self):
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT)
glLoadIdentity()
glTranslatef(0.0, 0.0, -5.0)
glRotatef(self.rotate_angle, 1, 1, 1)
glBegin(GL_QUADS)
glColor3f(1.0, 0.0, 0.0)
glVertex3f(1.0, 1.0, -1.0)
glVertex3f(-1.0, 1.0, -1.0)
glVertex3f(-1.0, 1.0, 1.0)
glVertex3f(1.0, 1.0, 1.0)
glColor3f(0.0, 1.0, 0.0)
glVertex3f(1.0, -1.0, 1.0)
glVertex3f(-1.0, -1.0, 1.0)
glVertex3f(-1.0, -1.0, -1.0)
glVertex3f(1.0, -1.0, -1.0)
glColor3f(0.0, 0.0, 1.0)
glVertex3f(1.0, 1.0, 1.0)
glVertex3f(-1.0, 1.0, 1.0)
glVertex3f(-1.0, -1.0, 1.0)
glVertex3f(1.0, -1.0, 1.0)
glColor3f(1.0, 1.0, 0.0)
glVertex3f(1.0, -1.0, -1.0)
glVertex3f(-1.0, -1.0, -1.0)
glVertex3f(-1.0, 1.0, -1.0)
glVertex3f(1.0, 1.0, -1.0)
glColor3f(0.0, 1.0, 1.0)
glVertex3f(-1.0, 1.0, 1.0)
glVertex3f(-1.0, 1.0, -1.0)
glVertex3f(-1.0, -1.0, -1.0)
glVertex3f(-1.0, -1.0, 1.0)
glColor3f(1.0, 0.0, 1.0)
glVertex3f(1.0, 1.0, -1.0)
glVertex3f(1.0, 1.0, 1.0)
glVertex3f(1.0, -1.0, 1.0)
glVertex3f(1.0, -1.0, -1.0)
glEnd()
self.rotate_angle += 1
Explanation:
- glClear(...) clears color and depth buffers
- glTranslatef(...) moves the scene back so the cube is visible
- glRotatef(...) applies rotation
- glBegin(GL_QUADS)/glVertex3f(...) draws each face
- incrementing rotate_angle creates continuous rotation.
Step 6: Add a Timer for Animation
We use a timer event to call update() repeatedly so paintGL() is called each frame.
def timerEvent(self, event):
self.update()
Explanation: update() schedules a repaint, producing smooth animation when called repeatedly by the timer.
Output
A new window will open and you will see a 3D cube rotating continuously, similar to the preview shown below: