杰瑞科技汇

Python如何结合OpenGL开发CAD系统?

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:

  1. 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.
  2. Scene Management: Keeping track of all objects in your scene, their positions, rotations, and other properties.

  3. 3D Transformation: Applying translation, rotation, and scaling to objects in 3D space using matrices.

  4. 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.
  5. 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:

  1. 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
  2. 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
  3. 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:

  1. Save the three files (simple_cad.py, vertex_shader.glsl, fragment_shader.glsl) in the same directory.
  2. Make sure you have installed the required libraries: pip install pyglet PyOpenGL numpy
  3. 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:

  1. Drawing Primitives:

    • Add buttons to switch between "Select", "Line", "Box", "Circle" modes.
    • In on_mouse_press and on_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 CADViewer class.
  2. 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.
  3. 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.
  4. 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.
  5. 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.

  6. 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

分享:
扫描分享到社交APP
上一篇
下一篇