Search code examples
pythonopenglpyqtglslpython-moderngl

Z dimension disappears in moderngl?


I've been trying to rotate an object but upon rotation, I realized it was just flat. What is strange is that I can clearly see the inputs for the z dim are there, it just isn't being accounted for. Here is my code:

import moderngl
from PyQt5 import QtOpenGL, QtCore, QtGui
from PyQt5.QtCore import Qt, pyqtSignal
import numpy as np
from pyrr import matrix44

cube_verts4 = np.array([
    -1.0, 1.0, -1.0, 1.0,
    -1.0, -1.0, -1.0, 1.0,
    1.0, -1.0, -1.0, 1.0,
    1.0, 1.0, -1.0, 1.0,
    -1.0, 1.0, 1.0, 1.0,
    -1.0, -1.0, 1.0, 1.0,
    1.0, -1.0, 1.0, 1.0,
    1.0, 1.0, 1.0, 1.0,
], dtype=np.float32)

cube_ibo_idxs = np.array([
    0, 1, 2,
    2, 3, 1,
    3, 2, 6,
    6, 5, 3,
    5, 6, 7,
    7, 4, 5,
    4, 7, 1,
    1, 0, 4,
    0, 3, 5,
    5, 4, 0,
    1, 7, 6,
    6, 2, 1

], dtype=np.int32)


class OpenGLWindowWidget(QtOpenGL.QGLWidget):
    vsync = True
    remove_event = pyqtSignal(str)

    def __init__(self, **kwargs):
        super().__init__(**kwargs)
        self.gl_version = (4, 3)
        fmt = QtOpenGL.QGLFormat()
        # need compute shader stuff
        fmt.setVersion(self.gl_version[0], self.gl_version[1])
        fmt.setProfile(QtOpenGL.QGLFormat.CoreProfile)
        fmt.setDepthBufferSize(24)
        fmt.setDoubleBuffer(True)
        fmt.setSwapInterval(1 if self.vsync else 0)
        self.ctx = None
        self.timer = QtCore.QTimer(self)
        self.timer.timeout.connect(self.update)
        self.timer.start(16)
        self.last_mouse_pos = None

        self.rotation_x = 0
        self.rotation_y = 0

    def mousePressEvent(self, event: QtGui.QMouseEvent) -> None:
        self.last_mouse_pos = event.pos()

    def mouseMoveEvent(self, event: QtGui.QMouseEvent) -> None:
        dx = event.x() - self.last_mouse_pos.x()
        dy = event.y() - self.last_mouse_pos.y()
        if event.buttons() & Qt.LeftButton:
            self.rotation_x += dx * 0.01
            self.rotation_y += dy * 0.01

        self.last_mouse_pos = event.pos()

    @property
    def gl_version_code(self) -> int:
        return self.gl_version[0] * 100 + self.gl_version[1] * 10

    @property
    def aspect_ratio(self):
        return self.width() / self.height()

    def initializeGL(self) -> None:
        self.ctx = moderngl.create_context(
            require=self.gl_version_code)
        self.prog = self.ctx.program(
            vertex_shader='''
                               #version 330
                               in vec4 vertex;
                               in float power;
                               uniform mat4 mvp_matrix;
                               void main() {
                                   gl_Position = vec4(mvp_matrix * vertex);
                               }
                           ''',
            fragment_shader='''
                                   #version 330
                                   out vec4 color;
                                   void main() {
                                       color = vec4(0.0, 0.0, 0.0, 1.0);
                                   }
                                   ''',
        )

        self.mvp_matrix = self.prog["mvp_matrix"]

        self.vbo = self.ctx.buffer(
            cube_verts4.astype('f4').tobytes())
        self.ibo = self.ctx.buffer(
            cube_ibo_idxs.astype('i4').tobytes())
        vao_content = [
            # 4 floats are assigned to the 'in' variable named 'vertex' in the shader code
            (self.vbo, '4f', 'vertex'),
        ]
        self.vao = self.ctx.vertex_array(self.prog, vao_content,
                                         self.ibo)

    def paintGL(self):
        target_width = 2
        target_height = 2
        r_aspect_ratio = target_width / target_height
        if self.aspect_ratio > r_aspect_ratio:
            v_a = self.aspect_ratio / r_aspect_ratio
            projection = matrix44.create_orthogonal_projection_matrix(
                -v_a * target_width / 2.0,
                v_a * target_width / 2.0, -target_height / 2.0,
                target_height / 2.0, 0, 100, dtype=np.float32)
        else:
            a_v = r_aspect_ratio / self.aspect_ratio
            projection = matrix44.create_orthogonal_projection_matrix(
                -target_width / 2.0, target_width / 2.0,
                -a_v * target_height / 2.0,
                a_v * target_height / 2.0,
                0, 100, dtype=np.float32)

        rotate = matrix44.create_from_y_rotation(
            self.rotation_x) * matrix44.create_from_x_rotation(
            self.rotation_y)

        self.mvp_matrix.write((rotate * projection).astype('f4').tobytes())
        self.ctx.viewport = (0, 0, self.width(), self.height())
        self.ctx.clear(0.0, 1.0, 1.0)
        self.vao.render()
        self.ctx.finish()

This is what I get out of it when I rotate.

enter image description here

enter image description here

enter image description here

I would have expected the silhouette of a cube, instead i get the silhouette of a plane.

I'm not even sure what could be causing this effect. I initially thought it was something to do with vec3's alignment, but I replaced my vertex + vbo code to use vec4s instead and it still doesn't work. I'm at a loss for how my depth is being "removed". I'm not familiar with pyrr so maybe some how the matrix transformation there is incorrect?


Solution

  • The element indices don't form a cube. Use the following indices:

    cube_ibo_idxs = np.array([
        0, 1, 2,   0, 2, 3,
        3, 2, 6,   3, 6, 7,
        7, 6, 5,   7, 5, 4,
        7, 4, 0,   7, 0, 3,
        4, 5, 1,   4, 1, 0,
        1, 5, 6,   1, 6, 2
    ], dtype=np.int32)
    

    pyrr Matrix44 operations return a numpy.array.
    for array, * means element-wise multiplication, while @ means matrix multiplication. See array. So you'Ve to use @ isntead of *. Alternatively you can use numpy.matmul.

    In the orthographic projection the near plane is set 0 and the far plane is set 100.

    projection = matrix44.create_orthogonal_projection_matrix(
                   v_a * -target_width / 2.0, v_a * target_width / 2.0,
                   -target_height / 2.0, target_height / 2.0,
                   0, 100, dtype=np.float32)
    

    Since the center of the geometry at (0, 0, 0), the cube mesh is partially clipped by the near plane of the cuboid view volume. Either change the near plane (e.g. -100) or draw the cube inbetween the near and far plane. This means you've to translate the mesh along the z axis. Since the (view space) z-axis points out of the view port (in Right-handed system), you've to translate the mesh in negative z direction (e.g. -3):

    rotate = matrix44.create_from_y_rotation(-self.rotation_x) @ \
             matrix44.create_from_x_rotation(-self.rotation_y) @ \
             matrix44.create_from_translation(np.array([0, 0, -3], dtype=np.float32)) 
    

    Further I recommend to enable the Depth Test. See ModernGL - Context:

    self.ctx.enable(moderngl.DEPTH_TEST)
    

    Use following function to draw the geometry:

    def paintGL(self):
        target_width = 4
        target_height = 4
        r_aspect_ratio = target_width / target_height
        if self.aspect_ratio > r_aspect_ratio:
            v_a = self.aspect_ratio / r_aspect_ratio
            projection = matrix44.create_orthogonal_projection_matrix(
                v_a * -target_width / 2.0, v_a * target_width / 2.0,
                -target_height / 2.0, target_height / 2.0,
                0, 100, dtype=np.float32)
        else:
            a_v = r_aspect_ratio / self.aspect_ratio
            projection = matrix44.create_orthogonal_projection_matrix(
                -target_width / 2.0, target_width / 2.0,
                -a_v * target_height / 2.0, a_v * target_height / 2.0,
                0, 100, dtype=np.float32)
    
        rotate = matrix44.create_from_y_rotation(-self.rotation_x) @ \
                    matrix44.create_from_x_rotation(-self.rotation_y) @ \
                    matrix44.create_from_translation(np.array([0, 0, -3], dtype=np.float32))      
    
        self.mvp_matrix.write((rotate @ projection).astype('f4').tobytes())
        self.ctx.viewport = (0, 0, self.width(), self.height())
        self.ctx.clear(0.0, 1.0, 1.0)
        self.ctx.enable(moderngl.DEPTH_TEST)
        self.vao.render()
        self.ctx.finish()
    

    If you use the following vertex shader

    #version 330
    in vec4 vertex;
    in float power;
    out vec4 v_clip_pos; 
    uniform mat4 mvp_matrix;
    void main() {
        v_clip_pos = mvp_matrix * vertex;
        gl_Position = v_clip_pos;
    }
    

    and fragment shader

    #version 330
    in vec4 v_clip_pos; 
    out vec4 color;
    void main() {
    
        vec3  ndc_pos = v_clip_pos.xyz / v_clip_pos.w;
        vec3  dx      = dFdx( ndc_pos );
        vec3  dy      = dFdy( ndc_pos );
    
        vec3 N = normalize(cross(dx, dy));
        N *= sign(N.z);
        vec3 L = vec3(0.0, 0.0, 1.0); 
        float NdotL = dot(N, L); 
    
        vec3 diffuse_color = vec3(0.5) * NdotL;
        color              = vec4( diffuse_color.rgb, 1.0 );
    }
    

    then you can achiev a slight 3D effect.