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