1#!/usr/bin/env python 2""" pygame.examples.glcube 3 4Draw a cube on the screen. 5 6 7 8Amazing. 9 10Every frame we orbit the camera around a small amount 11creating the illusion of a spinning object. 12 13First we setup some points of a multicolored cube. Then we then go through 14a semi-unoptimized loop to draw the cube points onto the screen. 15 16OpenGL does all the hard work for us. :] 17 18 19Keyboard Controls 20----------------- 21 22* ESCAPE key to quit 23* f key to toggle fullscreen. 24 25""" 26import math 27import ctypes 28 29import pygame as pg 30 31try: 32 import OpenGL.GL as GL 33 import OpenGL.GLU as GLU 34except ImportError: 35 print("pyopengl missing. The GLCUBE example requires: pyopengl numpy") 36 raise SystemExit 37 38try: 39 from numpy import array, dot, eye, zeros, float32, uint32 40except ImportError: 41 print("numpy missing. The GLCUBE example requires: pyopengl numpy") 42 raise SystemExit 43 44 45# do we want to use the 'modern' OpenGL API or the old one? 46# This example shows you how to do both. 47USE_MODERN_GL = True 48 49# Some simple data for a colored cube here we have the 3D point position 50# and color for each corner. A list of indices describes each face, and a 51# list of indices describes each edge. 52 53CUBE_POINTS = ( 54 (0.5, -0.5, -0.5), 55 (0.5, 0.5, -0.5), 56 (-0.5, 0.5, -0.5), 57 (-0.5, -0.5, -0.5), 58 (0.5, -0.5, 0.5), 59 (0.5, 0.5, 0.5), 60 (-0.5, -0.5, 0.5), 61 (-0.5, 0.5, 0.5), 62) 63 64# colors are 0-1 floating values 65CUBE_COLORS = ( 66 (1, 0, 0), 67 (1, 1, 0), 68 (0, 1, 0), 69 (0, 0, 0), 70 (1, 0, 1), 71 (1, 1, 1), 72 (0, 0, 1), 73 (0, 1, 1), 74) 75 76CUBE_QUAD_VERTS = ( 77 (0, 1, 2, 3), 78 (3, 2, 7, 6), 79 (6, 7, 5, 4), 80 (4, 5, 1, 0), 81 (1, 5, 7, 2), 82 (4, 0, 3, 6), 83) 84 85CUBE_EDGES = ( 86 (0, 1), 87 (0, 3), 88 (0, 4), 89 (2, 1), 90 (2, 3), 91 (2, 7), 92 (6, 3), 93 (6, 4), 94 (6, 7), 95 (5, 1), 96 (5, 4), 97 (5, 7), 98) 99 100 101def translate(matrix, x=0.0, y=0.0, z=0.0): 102 """ 103 Translate (move) a matrix in the x, y and z axes. 104 105 :param matrix: Matrix to translate. 106 :param x: direction and magnitude to translate in x axis. Defaults to 0. 107 :param y: direction and magnitude to translate in y axis. Defaults to 0. 108 :param z: direction and magnitude to translate in z axis. Defaults to 0. 109 :return: The translated matrix. 110 """ 111 translation_matrix = array( 112 [ 113 [1.0, 0.0, 0.0, x], 114 [0.0, 1.0, 0.0, y], 115 [0.0, 0.0, 1.0, z], 116 [0.0, 0.0, 0.0, 1.0], 117 ], 118 dtype=matrix.dtype, 119 ).T 120 matrix[...] = dot(matrix, translation_matrix) 121 return matrix 122 123 124def frustum(left, right, bottom, top, znear, zfar): 125 """ 126 Build a perspective matrix from the clipping planes, or camera 'frustrum' 127 volume. 128 129 :param left: left position of the near clipping plane. 130 :param right: right position of the near clipping plane. 131 :param bottom: bottom position of the near clipping plane. 132 :param top: top position of the near clipping plane. 133 :param znear: z depth of the near clipping plane. 134 :param zfar: z depth of the far clipping plane. 135 136 :return: A perspective matrix. 137 """ 138 perspective_matrix = zeros((4, 4), dtype=float32) 139 perspective_matrix[0, 0] = +2.0 * znear / (right - left) 140 perspective_matrix[2, 0] = (right + left) / (right - left) 141 perspective_matrix[1, 1] = +2.0 * znear / (top - bottom) 142 perspective_matrix[3, 1] = (top + bottom) / (top - bottom) 143 perspective_matrix[2, 2] = -(zfar + znear) / (zfar - znear) 144 perspective_matrix[3, 2] = -2.0 * znear * zfar / (zfar - znear) 145 perspective_matrix[2, 3] = -1.0 146 return perspective_matrix 147 148 149def perspective(fovy, aspect, znear, zfar): 150 """ 151 Build a perspective matrix from field of view, aspect ratio and depth 152 planes. 153 154 :param fovy: the field of view angle in the y axis. 155 :param aspect: aspect ratio of our view port. 156 :param znear: z depth of the near clipping plane. 157 :param zfar: z depth of the far clipping plane. 158 159 :return: A perspective matrix. 160 """ 161 h = math.tan(fovy / 360.0 * math.pi) * znear 162 w = h * aspect 163 return frustum(-w, w, -h, h, znear, zfar) 164 165 166def rotate(matrix, angle, x, y, z): 167 """ 168 Rotate a matrix around an axis. 169 170 :param matrix: The matrix to rotate. 171 :param angle: The angle to rotate by. 172 :param x: x of axis to rotate around. 173 :param y: y of axis to rotate around. 174 :param z: z of axis to rotate around. 175 176 :return: The rotated matrix 177 """ 178 angle = math.pi * angle / 180 179 c, s = math.cos(angle), math.sin(angle) 180 n = math.sqrt(x * x + y * y + z * z) 181 x, y, z = x / n, y / n, z / n 182 cx, cy, cz = (1 - c) * x, (1 - c) * y, (1 - c) * z 183 rotation_matrix = array( 184 [ 185 [cx * x + c, cy * x - z * s, cz * x + y * s, 0], 186 [cx * y + z * s, cy * y + c, cz * y - x * s, 0], 187 [cx * z - y * s, cy * z + x * s, cz * z + c, 0], 188 [0, 0, 0, 1], 189 ], 190 dtype=matrix.dtype, 191 ).T 192 matrix[...] = dot(matrix, rotation_matrix) 193 return matrix 194 195 196class Rotation: 197 """ 198 Data class that stores rotation angles in three axes. 199 """ 200 201 def __init__(self): 202 self.theta = 20 203 self.phi = 40 204 self.psi = 25 205 206 207def drawcube_old(): 208 """ 209 Draw the cube using the old open GL methods pre 3.2 core context. 210 """ 211 allpoints = list(zip(CUBE_POINTS, CUBE_COLORS)) 212 213 GL.glBegin(GL.GL_QUADS) 214 for face in CUBE_QUAD_VERTS: 215 for vert in face: 216 pos, color = allpoints[vert] 217 GL.glColor3fv(color) 218 GL.glVertex3fv(pos) 219 GL.glEnd() 220 221 GL.glColor3f(1.0, 1.0, 1.0) 222 GL.glBegin(GL.GL_LINES) 223 for line in CUBE_EDGES: 224 for vert in line: 225 pos, color = allpoints[vert] 226 GL.glVertex3fv(pos) 227 228 GL.glEnd() 229 230 231def init_gl_stuff_old(): 232 """ 233 Initialise open GL, prior to core context 3.2 234 """ 235 GL.glEnable(GL.GL_DEPTH_TEST) # use our zbuffer 236 237 # setup the camera 238 GL.glMatrixMode(GL.GL_PROJECTION) 239 GL.glLoadIdentity() 240 GLU.gluPerspective(45.0, 640 / 480.0, 0.1, 100.0) # setup lens 241 GL.glTranslatef(0.0, 0.0, -3.0) # move back 242 GL.glRotatef(25, 1, 0, 0) # orbit higher 243 244 245def init_gl_modern(display_size): 246 """ 247 Initialise open GL in the 'modern' open GL style for open GL versions 248 greater than 3.1. 249 250 :param display_size: Size of the window/viewport. 251 """ 252 253 # Create shaders 254 # -------------------------------------- 255 vertex_code = """ 256 257 #version 150 258 uniform mat4 model; 259 uniform mat4 view; 260 uniform mat4 projection; 261 262 uniform vec4 colour_mul; 263 uniform vec4 colour_add; 264 265 in vec4 vertex_colour; // vertex colour in 266 in vec3 vertex_position; 267 268 out vec4 vertex_color_out; // vertex colour out 269 void main() 270 { 271 vertex_color_out = (colour_mul * vertex_colour) + colour_add; 272 gl_Position = projection * view * model * vec4(vertex_position, 1.0); 273 } 274 275 """ 276 277 fragment_code = """ 278 #version 150 279 in vec4 vertex_color_out; // vertex colour from vertex shader 280 out vec4 fragColor; 281 void main() 282 { 283 fragColor = vertex_color_out; 284 } 285 """ 286 287 program = GL.glCreateProgram() 288 vertex = GL.glCreateShader(GL.GL_VERTEX_SHADER) 289 fragment = GL.glCreateShader(GL.GL_FRAGMENT_SHADER) 290 GL.glShaderSource(vertex, vertex_code) 291 GL.glCompileShader(vertex) 292 293 # this logs issues the shader compiler finds. 294 log = GL.glGetShaderInfoLog(vertex) 295 if isinstance(log, bytes): 296 log = log.decode() 297 for line in log.split("\n"): 298 print(line) 299 300 GL.glAttachShader(program, vertex) 301 GL.glShaderSource(fragment, fragment_code) 302 GL.glCompileShader(fragment) 303 304 # this logs issues the shader compiler finds. 305 log = GL.glGetShaderInfoLog(fragment) 306 if isinstance(log, bytes): 307 log = log.decode() 308 for line in log.split("\n"): 309 print(line) 310 311 GL.glAttachShader(program, fragment) 312 GL.glValidateProgram(program) 313 GL.glLinkProgram(program) 314 315 GL.glDetachShader(program, vertex) 316 GL.glDetachShader(program, fragment) 317 GL.glUseProgram(program) 318 319 # Create vertex buffers and shader constants 320 # ------------------------------------------ 321 322 # Cube Data 323 vertices = zeros( 324 8, [("vertex_position", float32, 3), ("vertex_colour", float32, 4)] 325 ) 326 327 vertices["vertex_position"] = [ 328 [1, 1, 1], 329 [-1, 1, 1], 330 [-1, -1, 1], 331 [1, -1, 1], 332 [1, -1, -1], 333 [1, 1, -1], 334 [-1, 1, -1], 335 [-1, -1, -1], 336 ] 337 338 vertices["vertex_colour"] = [ 339 [0, 1, 1, 1], 340 [0, 0, 1, 1], 341 [0, 0, 0, 1], 342 [0, 1, 0, 1], 343 [1, 1, 0, 1], 344 [1, 1, 1, 1], 345 [1, 0, 1, 1], 346 [1, 0, 0, 1], 347 ] 348 349 filled_cube_indices = array( 350 [ 351 0, 352 1, 353 2, 354 0, 355 2, 356 3, 357 0, 358 3, 359 4, 360 0, 361 4, 362 5, 363 0, 364 5, 365 6, 366 0, 367 6, 368 1, 369 1, 370 6, 371 7, 372 1, 373 7, 374 2, 375 7, 376 4, 377 3, 378 7, 379 3, 380 2, 381 4, 382 7, 383 6, 384 4, 385 6, 386 5, 387 ], 388 dtype=uint32, 389 ) 390 391 outline_cube_indices = array( 392 [0, 1, 1, 2, 2, 3, 3, 0, 4, 7, 7, 6, 6, 5, 5, 4, 0, 5, 1, 6, 2, 7, 3, 4], 393 dtype=uint32, 394 ) 395 396 shader_data = {"buffer": {}, "constants": {}} 397 398 GL.glBindVertexArray(GL.glGenVertexArrays(1)) # Have to do this first 399 400 shader_data["buffer"]["vertices"] = GL.glGenBuffers(1) 401 GL.glBindBuffer(GL.GL_ARRAY_BUFFER, shader_data["buffer"]["vertices"]) 402 GL.glBufferData(GL.GL_ARRAY_BUFFER, vertices.nbytes, vertices, GL.GL_DYNAMIC_DRAW) 403 404 stride = vertices.strides[0] 405 offset = ctypes.c_void_p(0) 406 407 loc = GL.glGetAttribLocation(program, "vertex_position") 408 GL.glEnableVertexAttribArray(loc) 409 GL.glVertexAttribPointer(loc, 3, GL.GL_FLOAT, False, stride, offset) 410 411 offset = ctypes.c_void_p(vertices.dtype["vertex_position"].itemsize) 412 413 loc = GL.glGetAttribLocation(program, "vertex_colour") 414 GL.glEnableVertexAttribArray(loc) 415 GL.glVertexAttribPointer(loc, 4, GL.GL_FLOAT, False, stride, offset) 416 417 shader_data["buffer"]["filled"] = GL.glGenBuffers(1) 418 GL.glBindBuffer(GL.GL_ELEMENT_ARRAY_BUFFER, shader_data["buffer"]["filled"]) 419 GL.glBufferData( 420 GL.GL_ELEMENT_ARRAY_BUFFER, 421 filled_cube_indices.nbytes, 422 filled_cube_indices, 423 GL.GL_STATIC_DRAW, 424 ) 425 426 shader_data["buffer"]["outline"] = GL.glGenBuffers(1) 427 GL.glBindBuffer(GL.GL_ELEMENT_ARRAY_BUFFER, shader_data["buffer"]["outline"]) 428 GL.glBufferData( 429 GL.GL_ELEMENT_ARRAY_BUFFER, 430 outline_cube_indices.nbytes, 431 outline_cube_indices, 432 GL.GL_STATIC_DRAW, 433 ) 434 435 shader_data["constants"]["model"] = GL.glGetUniformLocation(program, "model") 436 GL.glUniformMatrix4fv(shader_data["constants"]["model"], 1, False, eye(4)) 437 438 shader_data["constants"]["view"] = GL.glGetUniformLocation(program, "view") 439 view = translate(eye(4), z=-6) 440 GL.glUniformMatrix4fv(shader_data["constants"]["view"], 1, False, view) 441 442 shader_data["constants"]["projection"] = GL.glGetUniformLocation( 443 program, "projection" 444 ) 445 GL.glUniformMatrix4fv(shader_data["constants"]["projection"], 1, False, eye(4)) 446 447 # This colour is multiplied with the base vertex colour in producing 448 # the final output 449 shader_data["constants"]["colour_mul"] = GL.glGetUniformLocation( 450 program, "colour_mul" 451 ) 452 GL.glUniform4f(shader_data["constants"]["colour_mul"], 1, 1, 1, 1) 453 454 # This colour is added on to the base vertex colour in producing 455 # the final output 456 shader_data["constants"]["colour_add"] = GL.glGetUniformLocation( 457 program, "colour_add" 458 ) 459 GL.glUniform4f(shader_data["constants"]["colour_add"], 0, 0, 0, 0) 460 461 # Set GL drawing data 462 # ------------------- 463 GL.glClearColor(0, 0, 0, 0) 464 GL.glPolygonOffset(1, 1) 465 GL.glEnable(GL.GL_LINE_SMOOTH) 466 GL.glBlendFunc(GL.GL_SRC_ALPHA, GL.GL_ONE_MINUS_SRC_ALPHA) 467 GL.glDepthFunc(GL.GL_LESS) 468 GL.glHint(GL.GL_LINE_SMOOTH_HINT, GL.GL_NICEST) 469 GL.glLineWidth(1.0) 470 471 projection = perspective(45.0, display_size[0] / float(display_size[1]), 2.0, 100.0) 472 GL.glUniformMatrix4fv(shader_data["constants"]["projection"], 1, False, projection) 473 474 return shader_data, filled_cube_indices, outline_cube_indices 475 476 477def draw_cube_modern(shader_data, filled_cube_indices, outline_cube_indices, rotation): 478 """ 479 Draw a cube in the 'modern' Open GL style, for post 3.1 versions of 480 open GL. 481 482 :param shader_data: compile vertex & pixel shader data for drawing a cube. 483 :param filled_cube_indices: the indices to draw the 'filled' cube. 484 :param outline_cube_indices: the indices to draw the 'outline' cube. 485 :param rotation: the current rotations to apply. 486 """ 487 488 GL.glClear(GL.GL_COLOR_BUFFER_BIT | GL.GL_DEPTH_BUFFER_BIT) 489 490 # Filled cube 491 GL.glDisable(GL.GL_BLEND) 492 GL.glEnable(GL.GL_DEPTH_TEST) 493 GL.glEnable(GL.GL_POLYGON_OFFSET_FILL) 494 GL.glUniform4f(shader_data["constants"]["colour_mul"], 1, 1, 1, 1) 495 GL.glUniform4f(shader_data["constants"]["colour_add"], 0, 0, 0, 0.0) 496 GL.glBindBuffer(GL.GL_ELEMENT_ARRAY_BUFFER, shader_data["buffer"]["filled"]) 497 GL.glDrawElements( 498 GL.GL_TRIANGLES, len(filled_cube_indices), GL.GL_UNSIGNED_INT, None 499 ) 500 501 # Outlined cube 502 GL.glDisable(GL.GL_POLYGON_OFFSET_FILL) 503 GL.glEnable(GL.GL_BLEND) 504 GL.glUniform4f(shader_data["constants"]["colour_mul"], 0, 0, 0, 0.0) 505 GL.glUniform4f(shader_data["constants"]["colour_add"], 1, 1, 1, 1.0) 506 GL.glBindBuffer(GL.GL_ELEMENT_ARRAY_BUFFER, shader_data["buffer"]["outline"]) 507 GL.glDrawElements(GL.GL_LINES, len(outline_cube_indices), GL.GL_UNSIGNED_INT, None) 508 509 # Rotate cube 510 # rotation.theta += 1.0 # degrees 511 rotation.phi += 1.0 # degrees 512 # rotation.psi += 1.0 # degrees 513 model = eye(4, dtype=float32) 514 # rotate(model, rotation.theta, 0, 0, 1) 515 rotate(model, rotation.phi, 0, 1, 0) 516 rotate(model, rotation.psi, 1, 0, 0) 517 GL.glUniformMatrix4fv(shader_data["constants"]["model"], 1, False, model) 518 519 520def main(): 521 """run the demo""" 522 523 # initialize pygame and setup an opengl display 524 pg.init() 525 526 gl_version = (3, 0) # GL Version number (Major, Minor) 527 if USE_MODERN_GL: 528 gl_version = (3, 2) # GL Version number (Major, Minor) 529 530 # By setting these attributes we can choose which Open GL Profile 531 # to use, profiles greater than 3.2 use a different rendering path 532 pg.display.gl_set_attribute(pg.GL_CONTEXT_MAJOR_VERSION, gl_version[0]) 533 pg.display.gl_set_attribute(pg.GL_CONTEXT_MINOR_VERSION, gl_version[1]) 534 pg.display.gl_set_attribute( 535 pg.GL_CONTEXT_PROFILE_MASK, pg.GL_CONTEXT_PROFILE_CORE 536 ) 537 538 fullscreen = False # start in windowed mode 539 540 display_size = (640, 480) 541 pg.display.set_mode(display_size, pg.OPENGL | pg.DOUBLEBUF | pg.RESIZABLE) 542 543 if USE_MODERN_GL: 544 gpu, f_indices, o_indices = init_gl_modern(display_size) 545 rotation = Rotation() 546 else: 547 init_gl_stuff_old() 548 549 going = True 550 while going: 551 # check for quit'n events 552 events = pg.event.get() 553 for event in events: 554 if event.type == pg.QUIT or ( 555 event.type == pg.KEYDOWN and event.key == pg.K_ESCAPE 556 ): 557 going = False 558 559 elif event.type == pg.KEYDOWN and event.key == pg.K_f: 560 if not fullscreen: 561 print("Changing to FULLSCREEN") 562 pg.display.set_mode( 563 (640, 480), pg.OPENGL | pg.DOUBLEBUF | pg.FULLSCREEN 564 ) 565 else: 566 print("Changing to windowed mode") 567 pg.display.set_mode((640, 480), pg.OPENGL | pg.DOUBLEBUF) 568 fullscreen = not fullscreen 569 if gl_version[0] >= 4 or (gl_version[0] == 3 and gl_version[1] >= 2): 570 gpu, f_indices, o_indices = init_gl_modern(display_size) 571 rotation = Rotation() 572 else: 573 init_gl_stuff_old() 574 575 if USE_MODERN_GL: 576 draw_cube_modern(gpu, f_indices, o_indices, rotation) 577 else: 578 # clear screen and move camera 579 GL.glClear(GL.GL_COLOR_BUFFER_BIT | GL.GL_DEPTH_BUFFER_BIT) 580 # orbit camera around by 1 degree 581 GL.glRotatef(1, 0, 1, 0) 582 drawcube_old() 583 584 pg.display.flip() 585 pg.time.wait(10) 586 587 pg.quit() 588 589 590if __name__ == "__main__": 591 main() 592