Search code examples
pythonopenglpyopengl

Attempting to update 500+ vertex positions in OpenGL causes a read access violation?


I am attempting to plot a large number of vertices (similar to a point cloud).

At the same time I am also trying to learn how to use VBOs and pass Numpy arrays in to the GLSL program and animate the vertex positions.

I think I have made some progress however I am limited to how large I can set the array_size parameter and anything over 500 typically causes an error: WindowsError: exception: access violation reading 0x0999B000

Sometimes it works (randomly) and I was just wondering if I am making a mistake in the way I allocate the memory or buffering the arrays?

Just to add a note, in the future I am hoping to update 2500+ vertex positions at one time. And was wondering how I could make that possible?

array_size = 100

#!/bin/env python
# coding: utf-8

import time
import numpy as np
from textwrap import dedent

from OpenGL.GL import *
from OpenGL.GL.shaders import compileShader, compileProgram

import pygame
from pygame.locals import *

##############################################################################
# OpenGL funcs
##############################################################################
buffers=None
shader = None
def init_gl():
    glEnable(GL_VERTEX_PROGRAM_POINT_SIZE) #allow the program to specify the point size

    global shader, buffers

    vertex_shader = compileShader(dedent('''

        uniform mat4 Projection = mat4(1);
        uniform mat4 ModelView = mat4(1);

        varying out vec3 _color;

        void main() {
            _color = gl_Color;
            gl_Position =  Projection * ModelView * gl_ModelViewProjectionMatrix * gl_Vertex;

            vec3 ndc = gl_Position.xyz / gl_Position.w ; // perspective divide.
            float zDist = 1.0-ndc.z ; // 1 is close (right up in your face,)
            // 0 is far (at the far plane)
            gl_PointSize = 25*zDist ; // between 0 and 50 now.

        }
        '''), GL_VERTEX_SHADER)
    fragment_shader = compileShader(dedent('''

        in vec3 _color;

        void main() {
            gl_FragColor = vec4(_color, 1.0); //gl_Color;
        }
        '''), GL_FRAGMENT_SHADER)
    shader = compileProgram(vertex_shader, fragment_shader)

    buffers=create_vbo()

yaw=0
pitch=0
def draw():
    global yaw, pitch
    glClear(GL_COLOR_BUFFER_BIT)# | GL_DEPTH_BUFFER_BIT)

    glMatrixMode(GL_MODELVIEW)
    glLoadIdentity()

    yaw+=0.39
    pitch+=0.27
    glTranslatef(0.0, 0.0, 0.0)
    glRotatef(yaw, 0, 1, 0)
    glRotatef(pitch, 1, 0, 0)

    setPoints()
    glFlush()

##############################################################################
# vertices
##############################################################################
array_size = 1000
scale = 0.15

#create dataset https://matplotlib.org/mpl_toolkits/mplot3d/tutorial.html
theta = np.linspace(-4 * np.pi, 4 * np.pi, array_size)
z = np.linspace(-2, 2, array_size)
r = z**2 + 1
x = r * np.sin(theta)
y = r * np.cos(theta)

vertices = np.dstack((x,y,z)) * scale
colors = np.tile(np.array([0.0, 1.0, 0.0]), (array_size,1)) #a bunch of green vertices
indices=np.arange(array_size)


def create_vbo():
    buffers = glGenBuffers(3)

    glBindBuffer(GL_ARRAY_BUFFER, buffers[0])
    glBufferData(GL_ARRAY_BUFFER,
            vertices.nbytes,  # byte size
            (ctypes.c_float*len(vertices.flat))(*vertices.flat),
            GL_STREAM_DRAW)

    glBindBuffer(GL_ARRAY_BUFFER, buffers[1])
    glBufferData(GL_ARRAY_BUFFER,
            colors.nbytes, # byte size
            (ctypes.c_float*len(colors.flat))(*colors.flat),
            GL_STATIC_DRAW)

    glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, buffers[2])
    glBufferData(GL_ELEMENT_ARRAY_BUFFER,
            indices.nbytes, # byte size
            (ctypes.c_uint*len(indices.flat))(*indices.flat),
            GL_STATIC_DRAW)
    return buffers

def draw_vbo():
    glEnableClientState(GL_VERTEX_ARRAY);
    glEnableClientState(GL_COLOR_ARRAY);

    glBindBuffer(GL_ARRAY_BUFFER, buffers[0]);
    glVertexPointer(3, GL_FLOAT, 0, None);

    glBindBuffer(GL_ARRAY_BUFFER, buffers[1]);
    glColorPointer(3, GL_FLOAT, 0, None);

    glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, buffers[2]);
    glDrawElements(GL_POINTS, indices.size, GL_UNSIGNED_INT, None);

    glDisableClientState(GL_COLOR_ARRAY)
    glDisableClientState(GL_VERTEX_ARRAY);


def setPoints():
    global shader

    glUseProgram(shader)
    draw_vbo()

    projection = np.array([#the matrix generated captured while using HTC Vive
        [ 0.75752085,  0.        ,  0.        ,  0.],
        [ 0.        ,  0.68160856,  0.        ,  0.],
        [ 0.05516453, -0.00299519, -1.00040019, -1.],
        [ 0.        ,  0.        , -0.20008004,  0.]
    ])
    modelview = np.array([#the matrix generated captured while using HTC Vive
        [ 0.99030989,  0.04490654,  0.13141415,  0.],
        [-0.01430531,  0.9742285 , -0.22510922,  0.],
        [-0.13813627,  0.22104797,  0.9654305 ,  0.],
        [-0.12975544, -0.9294402 , -1.06236947,  1.]
    ])

    glUniformMatrix4fv(glGetUniformLocation(shader, "Projection"), 1, False, projection)
    glUniformMatrix4fv(glGetUniformLocation(shader, "ModelView"), 1, False, modelview)

    glUseProgram(0)

##############################################################################
if __name__ == '__main__':

    pygame.init()
    pygame.display.set_mode((800, 600), HWSURFACE|OPENGL|DOUBLEBUF)

    init_gl()

    start_time = time.time()
    while time.time() - start_time < 5: #5 second animation
        draw()
        pygame.display.flip()

[UPDATE]

I believe I have answered my own question when I realized I was not casting my Numpy array's datatypes correctly. So by adding .astype() to my arrays when I create them I have managed to get 2000+ verts to show and animate :)

vertices = (np.dstack((x,y,z)) * scale).astype(np.float32)
colors = (np.tile(np.array([0.0, 1.0, 0.0]), (array_size,1))).astype(np.float32) #a bunch of green vertices
indices = np.arange(array_size).astype(np.uint32)

And here is the fixed example:

#!/bin/env python
# coding: utf-8

import time
import numpy as np
from textwrap import dedent

from OpenGL.GL import *
from OpenGL.GL.shaders import compileShader, compileProgram

import pygame
from pygame.locals import *

##############################################################################
# OpenGL funcs
##############################################################################
buffers=None
shader = None
def init_gl():
    glEnable(GL_VERTEX_PROGRAM_POINT_SIZE) #allow the program to specify the point size

    global shader, buffers

    vertex_shader = compileShader(dedent('''

        uniform mat4 Projection = mat4(1);
        uniform mat4 ModelView = mat4(1);

        varying out vec3 _color;

        void main() {
            _color = gl_Color;
            gl_Position =  Projection * ModelView * gl_ModelViewProjectionMatrix * gl_Vertex;

            vec3 ndc = gl_Position.xyz / gl_Position.w ; // perspective divide.
            float zDist = 1.0-ndc.z ; // 1 is close (right up in your face,)
            // 0 is far (at the far plane)
            gl_PointSize = 25*zDist ; // between 0 and 50 now.

        }
        '''), GL_VERTEX_SHADER)
    fragment_shader = compileShader(dedent('''

        in vec3 _color;

        void main() {
            gl_FragColor = vec4(_color, 1.0); //gl_Color;
        }
        '''), GL_FRAGMENT_SHADER)
    shader = compileProgram(vertex_shader, fragment_shader)

    buffers=create_vbo()

yaw=0
pitch=0
def draw():
    global yaw, pitch
    glClear(GL_COLOR_BUFFER_BIT)# | GL_DEPTH_BUFFER_BIT)

    glMatrixMode(GL_MODELVIEW)
    glLoadIdentity()

    yaw+=0.39
    pitch+=0.27
    glTranslatef(0.0, 0.0, 0.0)
    glRotatef(yaw, 0, 1, 0)
    glRotatef(pitch, 1, 0, 0)

    setPoints()
    glFlush()

##############################################################################
# vertices
##############################################################################
array_size = 2000
scale = 0.15

#create dataset https://matplotlib.org/mpl_toolkits/mplot3d/tutorial.html
theta = np.linspace(-4 * np.pi, 4 * np.pi, array_size)
z = np.linspace(-2, 2, array_size)
r = z**2 + 1
x = r * np.sin(theta)
y = r * np.cos(theta)

vertices = (np.dstack((x,y,z)) * scale).astype(np.float32)
colors = (np.tile(np.array([0.0, 1.0, 0.0]), (array_size,1))).astype(np.float32) #a bunch of green vertices
indices = np.arange(array_size).astype(np.uint)


def create_vbo():
    buffers = glGenBuffers(3)

    glBindBuffer(GL_ARRAY_BUFFER, buffers[0])
    glBufferData(GL_ARRAY_BUFFER,
            vertices.nbytes,  # byte size
            (ctypes.c_float*len(vertices.flat))(*vertices.flat),
            GL_STREAM_DRAW)

    glBindBuffer(GL_ARRAY_BUFFER, buffers[1])
    glBufferData(GL_ARRAY_BUFFER,
            colors.nbytes, # byte size
            (ctypes.c_float*len(colors.flat))(*colors.flat),
            GL_STATIC_DRAW)

    glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, buffers[2])
    glBufferData(GL_ELEMENT_ARRAY_BUFFER,
            indices.nbytes, # byte size
            (ctypes.c_uint*len(indices.flat))(*indices.flat),
            GL_STATIC_DRAW)
    return buffers

def draw_vbo():
    glEnableClientState(GL_VERTEX_ARRAY);
    glEnableClientState(GL_COLOR_ARRAY);

    glBindBuffer(GL_ARRAY_BUFFER, buffers[0]);
    glVertexPointer(3, GL_FLOAT, 0, None);

    glBindBuffer(GL_ARRAY_BUFFER, buffers[1]);
    glColorPointer(3, GL_FLOAT, 0, None);

    glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, buffers[2]);
    glDrawElements(GL_POINTS, indices.size, GL_UNSIGNED_INT, None);

    glDisableClientState(GL_COLOR_ARRAY)
    glDisableClientState(GL_VERTEX_ARRAY);


def setPoints():
    global shader

    glUseProgram(shader)
    draw_vbo()

    projection = np.array([#the matrix generated captured while using HTC Vive
        [ 0.75752085,  0.        ,  0.        ,  0.],
        [ 0.        ,  0.68160856,  0.        ,  0.],
        [ 0.05516453, -0.00299519, -1.00040019, -1.],
        [ 0.        ,  0.        , -0.20008004,  0.]
    ])
    modelview = np.array([#the matrix generated captured while using HTC Vive
        [ 0.99030989,  0.04490654,  0.13141415,  0.],
        [-0.01430531,  0.9742285 , -0.22510922,  0.],
        [-0.13813627,  0.22104797,  0.9654305 ,  0.],
        [-0.12975544, -0.9294402 , -1.06236947,  1.]
    ])

    glUniformMatrix4fv(glGetUniformLocation(shader, "Projection"), 1, False, projection)
    glUniformMatrix4fv(glGetUniformLocation(shader, "ModelView"), 1, False, modelview)

    glUseProgram(0)

##############################################################################
if __name__ == '__main__':

    pygame.init()
    pygame.display.set_mode((800, 600), HWSURFACE|OPENGL|DOUBLEBUF)

    init_gl()

    start_time = time.time()
    while time.time() - start_time < 5: #5 second animation
        draw()
        pygame.display.flip()

Solution

  • The problem is that you overall don't specify a dtype, which results in float64 getting chosen as the dtype.

    print(vertices.dtype)
    print(colors.dtype)
    print(indices.dtype)
    

    That should print "float32", "float32", "uint32". It however prints "float64", "float64", "int32".

    The problem with that is that now your data is double the size. Thus when you call glBufferData() you're telling the size of your data is double the the size of what it actually is (in contrast to what you're actually giving it).


    That is what triggers the access violation reading 0x0999B000, since your driver is trying to read data outside the bounds of the data you gave it.

    The easiest way to fix it would be to do the following after setting up vertices:

    vertices = vertices.astype(np.float32)
    colors = colors.astype(np.float32)
    indices = indices.astype(np.uint32)
    

    Now you'll be able to add as many vertices as your RAM and VRAM allows!


    Lastly to save yourself from future headaches, add the following before calling glBufferData() with vertices:

    assert vertices.dtype == np.float32
    

    Equally do so for colors and indices (use np.uint32 instead).

    This will trigger an assertion if the data type is any other than the desired data type.


    For indices less than 231-1 whether you use int32 or uint32 won't make a change. However it might be wise to go with the expected type, thus avoiding accidentally tripping yourself in the future. Even though you probably won't reach that amount of indices or anybody else for that matter. But better safe than sorry.