Of course! Using Python for OpenGL-based CAD (Computer-Aided Design) is an excellent project that combines graphics programming, geometry, and user interaction. It's a fantastic way to learn how 3D applications work under the hood.
Here’s a comprehensive guide covering the concepts, tools, libraries, and a practical example to get you started.
Core Concepts: What is a CAD Application?
A CAD application, even a simple one, needs to handle several key tasks:
-
3D Geometry Representation: How do you store a cube, a line, or a more complex shape? Common representations are:
- Polygonal Meshes (Vertices, Edges, Faces): The most common in real-time graphics. A cube is defined by 8 vertices, 12 edges, and 6 faces (each a quad or two triangles).
- Constructive Solid Geometry (CSG): Defining shapes as boolean operations (union, intersection, difference) on primitive objects (spheres, cubes, cylinders). This is more complex but very powerful.
- Parametric Curves/Surfaces: Using mathematical formulas (like Bézier or NURBS curves) to define smooth shapes.
-
Scene Management: Keeping track of all objects in your scene, their positions, rotations, and other properties.
-
3D Transformation: Applying translation, rotation, and scaling to objects in 3D space using matrices.
-
Rendering: The process of converting your 3D scene into a 2D image on your screen. This involves:
- The Vertex Shader: Processes the vertices of your geometry (position, color).
- The Fragment Shader: Determines the color of each pixel on the screen.
-
User Interaction (The "CAD" part): This is what makes it an application and not just a viewer.
- Camera Control: Orbiting, panning, and zooming around the model (Trackball or Arcball camera is standard).
- Object Manipulation: Selecting objects and moving, rotating, or scaling them.
- Drawing Tools: Creating new geometry (lines, rectangles, circles).
- Snapping: Snapping the cursor or new geometry to existing points, edges, or grids.
Essential Python Libraries
You'll need a few key libraries to build a CAD application. The most popular and modern combination is PyOpenGL for rendering and Pyglet or ModernGL for window and context management.
The Core Trio:
-
PyOpenGL: The Python bindings for OpenGL. It's the core of your rendering pipeline. You'll use it to define your shaders and send data to the GPU.
pip install PyOpenGL
-
Pyglet: A pure Python library for creating windows, handling user input (keyboard, mouse), and managing OpenGL contexts. It's lightweight and has a clean API, making it perfect for this kind of project.
pip install pyglet
-
ModernGL (Advanced Alternative): A wrapper for OpenGL that provides a much more "Pythonic" and modern API. It simplifies many complex OpenGL tasks (like buffer management) and uses shaders by default. If you find PyOpenGL too verbose, start with ModernGL.
pip install moderngl
Supporting Libraries:
- NumPy: Absolutely essential for handling large arrays of vertices, colors, and transformation matrices efficiently. Python lists are too slow.
pip install numpy
- PyQt / PySide: For building a complex user interface with toolbars, menus, and property panels. This is a big step up in complexity.
pip install PySide6(or PyQt6)
A Practical Example: A Simple CAD Viewer in PyOpenGL/Pyglet
Let's build a minimal but functional application. It will:
- Create a window.
- Render a simple 3D cube.
- Allow you to orbit the camera with the mouse.
- Use a modern approach with shaders.
Step 1: Project Setup
Create a file named simple_cad.py. We'll use the modern approach with vertex and fragment shaders.
Step 2: The Vertex Shader
This shader takes the position of a vertex and applies a model-view-projection matrix to it.
Create a file named vertex_shader.glsl:
# version 330 core
// Input from our Python program
layout (location = 0) in vec3 aPos;
// The matrix that combines View and Projection
uniform mat4 viewProjection;
// The matrix that transforms the object's local space to world space
uniform mat4 model;
void main()
{
// gl_Position is the final output position of the vertex
gl_Position = viewProjection * model * vec4(aPos, 1.0);
}
Step 3: The Fragment Shader
This shader simply sets every pixel of the triangle to a solid color.
Create a file named fragment_shader.glsl:
# version 330 core
// Output color to the screen
out vec4 FragColor;
void main()
{
FragColor = vec4(0.8f, 0.3f, 0.2f, 1.0f); // A nice orange color
}
Step 4: The Python Code (simple_cad.py)
This code ties everything together. It handles window creation, shader compilation, geometry setup, and the render loop.
import pyglet
from pyglet.gl import *
import numpy as np
import math
# --- Shader Program ---
def create_shader_program(vertex_source, fragment_source):
# Compile shaders
vertex_shader = glCreateShader(GL_VERTEX_SHADER)
glShaderSource(vertex_shader, vertex_source)
glCompileShader(vertex_shader)
fragment_shader = glCreateShader(GL_FRAGMENT_SHADER)
glShaderSource(fragment_shader, fragment_source)
glCompileShader(fragment_shader)
# Create program and link shaders
program = glCreateProgram()
glAttachShader(program, vertex_shader)
glAttachShader(program, fragment_shader)
glLinkProgram(program)
# Clean up shaders (they are now linked into the program)
glDeleteShader(vertex_shader)
glDeleteShader(fragment_shader)
return program
# --- Matrix Helper Functions ---
def create_perspective_matrix(fov, aspect, near, far):
f = 1.0 / math.tan(math.radians(fov) / 2.0)
return np.array([
[f / aspect, 0, 0, 0],
[0, f, 0, 0],
[0, 0, (far + near) / (near - far), (2 * far * near) / (near - far)],
[0, 0, -1, 0]
], dtype=np.float32)
def create_look_at_matrix(eye, target, up):
eye = np.array(eye, dtype=np.float32)
target = np.array(target, dtype=np.float32)
up = np.array(up, dtype=np.float32)
forward = target - eye
forward = forward / np.linalg.norm(forward)
right = np.cross(forward, up)
right = right / np.linalg.norm(right)
up = np.cross(right, forward)
view_matrix = np.identity(4, dtype=np.float32)
view_matrix[0, :3] = right
view_matrix[1, :3] = up
view_matrix[2, :3] = -forward
view_matrix[:3, 3] = -np.dot(view_matrix[:3, :3], eye)
return view_matrix
def create_rotation_matrix(angle_x, angle_y):
# Rotate around Y axis first, then X axis
cos_y, sin_y = math.cos(angle_y), math.sin(angle_y)
cos_x, sin_x = math.cos(angle_x), math.sin(angle_x)
rot_y = np.array([
[cos_y, 0, sin_y, 0],
[0, 1, 0, 0],
[-sin_y, 0, cos_y, 0],
[0, 0, 0, 1]
], dtype=np.float32)
rot_x = np.array([
[1, 0, 0, 0],
[0, cos_x, -sin_x, 0],
[0, sin_x, cos_x, 0],
[0, 0, 0, 1]
], dtype=np.float32)
return np.dot(rot_x, rot_y)
# --- CAD Application Class ---
class CADViewer(pyglet.window.Window):
def __init__(self, width, height, title="Simple CAD Viewer"):
super().__init__(width, height, title, resizable=True)
# Camera state
self.camera_distance = 10.0
self.camera_rotation_x = 0.0
self.camera_rotation_y = 0.0
self.last_mouse_x = 0
self.last_mouse_y = 0
# Load shaders
with open('vertex_shader.glsl', 'r') as f:
vertex_source = f.read()
with open('fragment_shader.glsl', 'r') as f:
fragment_source = f.read()
self.shader_program = create_shader_program(vertex_source, fragment_source)
glUseProgram(self.shader_program)
# Get uniform locations from the shader
self.mvp_matrix_location = glGetUniformLocation(self.shader_program, "viewProjection")
self.model_matrix_location = glGetUniformLocation(self.shader_program, "model")
# Define cube vertices
# A cube has 8 vertices. We'll use indices to define triangles.
vertices = np.array([
0.5, 0.5, -0.5, # Top-right-front
0.5, -0.5, -0.5, # Bottom-right-front
-0.5, -0.5, -0.5, # Bottom-left-front
-0.5, 0.5, -0.5, # Top-left-front
0.5, 0.5, 0.5, # Top-right-back
0.5, -0.5, 0.5, # Bottom-right-back
-0.5, -0.5, 0.5, # Bottom-left-back
-0.5, 0.5, 0.5, # Top-left-back
], dtype=np.float32)
# Define indices for 12 triangles (2 per face)
indices = np.array([
0, 1, 2, 2, 3, 0, # Front
4, 7, 6, 6, 5, 4, # Back
0, 4, 5, 5, 1, 0, # Right
2, 6, 7, 7, 3, 2, # Left
0, 3, 7, 7, 4, 0, # Top
1, 5, 6, 6, 2, 1 # Bottom
], dtype=np.uint32)
# Create and bind VAO (Vertex Array Object)
self.vao = GLuint(0)
glGenVertexArrays(1, byref(self.vao))
glBindVertexArray(self.vao)
# Create and bind VBO (Vertex Buffer Object) for vertices
self.vbo = GLuint(0)
glGenBuffers(1, byref(self.vbo))
glBindBuffer(GL_ARRAY_BUFFER, self.vbo)
glBufferData(GL_ARRAY_BUFFER, vertices.nbytes, vertices, GL_STATIC_DRAW)
# Tell OpenGL how to interpret the vertex data
# The '0' corresponds to 'layout (location = 0)' in the vertex shader
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * 4, c_void_p(0))
glEnableVertexAttribArray(0)
# Create and bind EBO (Element Buffer Object) for indices
self.ebo = GLuint(0)
glGenBuffers(1, byref(self.ebo))
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, self.ebo)
glBufferData(GL_ELEMENT_ARRAY_BUFFER, indices.nbytes, indices, GL_STATIC_DRAW)
# Unbind everything
glBindBuffer(GL_ARRAY_BUFFER, 0)
glBindVertexArray(0)
def on_draw(self):
self.clear()
glUseProgram(self.shader_program)
# --- Update Camera ---
# Perspective projection matrix
projection = create_perspective_matrix(45, self.width / self.height, 0.1, 100.0)
# View matrix (camera position based on spherical coordinates)
camera_x = self.camera_distance * math.sin(self.camera_rotation_y) * math.cos(self.camera_rotation_x)
camera_y = self.camera_distance * math.sin(self.camera_rotation_x)
camera_z = self.camera_distance * math.cos(self.camera_rotation_y) * math.cos(self.camera_rotation_x)
view = create_look_at_matrix(
eye=(camera_x, camera_y, camera_z), # Camera position
target=(0, 0, 0), # Look at the origin
up=(0, 1, 0) # Up vector
)
# Model matrix (identity for now, our cube is at the origin)
model = np.identity(4, dtype=np.float32)
# Combine matrices
view_projection = np.dot(projection, view)
# Send matrices to the shader
glUniformMatrix4fv(self.mvp_matrix_location, 1, GL_FALSE, view_projection)
glUniformMatrix4fv(self.model_matrix_location, 1, GL_FALSE, model)
# --- Draw the cube ---
glBindVertexArray(self.vao)
glDrawElements(GL_TRIANGLES, 36, GL_UNSIGNED_INT, c_void_p(0)) # 36 indices
glBindVertexArray(0)
def on_resize(self, width, height):
# Set the viewport to cover the entire window
glViewport(0, 0, width, height)
super().on_resize(width, height)
def on_mouse_drag(self, x, y, dx, dy, buttons, modifiers):
# Rotate camera with left mouse button
if buttons == pyglet.window.mouse.LEFT:
self.camera_rotation_y += dx * 0.01
self.camera_rotation_x += dy * 0.01
# Clamp vertical rotation to avoid gimbal lock
self.camera_rotation_x = max(-math.pi/2 + 0.1, min(math.pi/2 - 0.1, self.camera_rotation_x))
def on_mouse_scroll(self, x, y, scroll_x, scroll_y):
# Zoom camera with mouse wheel
self.camera_distance -= scroll_y * 0.5
self.camera_distance = max(2.0, min(50.0, self.camera_distance))
# --- Main Execution ---
if __name__ == '__main__':
window = CADViewer(800, 600)
pyglet.app.run()
How to Run:
- Save the three files (
simple_cad.py,vertex_shader.glsl,fragment_shader.glsl) in the same directory. - Make sure you have installed the required libraries:
pip install pyglet PyOpenGL numpy - Run the Python script:
python simple_cad.py
You should see an orange cube in a window. You can click and drag to orbit it and use the scroll wheel to zoom.
Next Steps: From Viewer to CAD Tool
This is just the foundation. Here’s how you can evolve this into a more capable CAD application:
-
Drawing Primitives:
- Add buttons to switch between "Select", "Line", "Box", "Circle" modes.
- In
on_mouse_pressandon_mouse_release, capture the start and end points. - Generate the vertices for the new shape (e.g., two vertices for a line, 8 for a box).
- Create a new VAO/VBO for this new object and store it in a list in your
CADViewerclass.
-
Object Selection:
- Picking: This is the key to selection. The standard method is "color picking."
- In a hidden off-screen render pass, render each object with a unique, solid color (e.g., object 1 is red, object 2 is green).
- When the user clicks, read the color of the pixel under the cursor.
- The color value directly tells you which object was selected.
-
Object Transformation (Gizmos):
- When an object is selected, draw 3D "handles" or "gizmos" around it (arrows for move, circles for rotate, squares for scale).
- Implement ray-casting to determine which gizmo the mouse is hovering over.
- Update the object's model matrix based on mouse movement.
-
Constructive Solid Geometry (CSG):
- This is much harder. You would need a library like OpenVDB (which has Python bindings) or implement your own mesh-boolean algorithms. This is an advanced topic.
-
File I/O:
Implement saving and loading your scene. A simple JSON format is good for storing object types, vertices, and transformations. For more compatibility, you could export to standard formats like OBJ or STL.
-
User Interface (UI):
- Integrate a library like Dear PyGui or PyQt/PySide to create a proper UI with toolbars, menus, and property editors. This allows users to change object properties like color, layer, or dimensions.
This journey is challenging but incredibly rewarding. Starting with a simple viewer like the one above is the perfect first step. Good luck
