From efdf72751a3552e870d46ba752d370122b0d6cf6 Mon Sep 17 00:00:00 2001 From: Matthias Goerner <1239022+unhyperbolic@users.noreply.github.com> Date: Sat, 27 Jan 2024 13:14:20 -0800 Subject: [PATCH] OpenGL: splitting all modern OpenGL classes into their own pyx files. --- opengl/CyOpenGL.pyx | 1129 +----------------- opengl/modern/data_based_texture.pyx | 45 + opengl/modern/glsl_perspective_view.pyx | 199 +++ opengl/modern/glsl_perspective_widget.pyx | 26 + opengl/modern/glsl_program.pyx | 334 ++++++ opengl/modern/image_based_texture.pyx | 8 + opengl/modern/simple_image_shader_widget.pyx | 269 +++++ opengl/modern/uniform_buffer_object.pyx | 119 ++ opengl/modern/vertex_buffer.pyx | 115 ++ 9 files changed, 1123 insertions(+), 1121 deletions(-) create mode 100644 opengl/modern/data_based_texture.pyx create mode 100644 opengl/modern/glsl_perspective_view.pyx create mode 100644 opengl/modern/glsl_perspective_widget.pyx create mode 100644 opengl/modern/glsl_program.pyx create mode 100644 opengl/modern/image_based_texture.pyx create mode 100644 opengl/modern/simple_image_shader_widget.pyx create mode 100644 opengl/modern/uniform_buffer_object.pyx create mode 100644 opengl/modern/vertex_buffer.pyx diff --git a/opengl/CyOpenGL.pyx b/opengl/CyOpenGL.pyx index 72bbe44d..6f9d6933 100644 --- a/opengl/CyOpenGL.pyx +++ b/opengl/CyOpenGL.pyx @@ -1597,1125 +1597,12 @@ class OpenGLOrthoWidget(OpenGLPerspectiveWidget): ############################################################################## # OpenGL objects widgets for modern OpenGL (OpenGL 3.2 or later) -cdef class DataBasedTexture: - cdef GLuint _textureName - - def __cinit__(self): - self._textureName = 0 - - def __init__(self, - unsigned int width, - unsigned int height, - unsigned char[:] rgba_data): - - if rgba_data.size != 4 * width * height: - raise RuntimeError("Length of rgba_data not matching") - - glGenTextures(1, &self._textureName) - glActiveTexture(GL_TEXTURE0) - glBindTexture(GL_TEXTURE_2D, self._textureName) - - glTexImage2D(GL_TEXTURE_2D, 0, - GL_RGBA, - width, - height, - 0, - GL_RGBA, - GL_UNSIGNED_BYTE, - &rgba_data[0]) - - glTexParameteri(GL_TEXTURE_2D, - GL_TEXTURE_MIN_FILTER, - GL_LINEAR) - glTexParameteri(GL_TEXTURE_2D, - GL_TEXTURE_MAG_FILTER, - GL_LINEAR) - - glBindTexture(GL_TEXTURE_2D, 0) - - def bind(self): - glBindTexture(GL_TEXTURE_2D, self._textureName) - - def unbind(self): - glBindTexture(GL_TEXTURE_2D, 0) - - def delete_resource(self): - glDeleteTextures(1, &self._textureName) - self._textureName = 0 - -class ImageBasedTexture(DataBasedTexture): - def __init__(self, texture_file): - w, h, rows, info = png.Reader(texture_file).asRGBA8() - data = bytearray(4 * w * h) - for i, row in enumerate(rows): - data[i * 4 * w : (i + 1) * 4 * w] = row - - super().__init__(w, h, data) - -cdef GLfloat* _convert_matrices_to_floats( - matrices, num_matrices, num_rows, num_columns): - cdef GLfloat * floats - floats = malloc( - num_matrices * num_rows * num_columns * sizeof(GLfloat)) - for i in range(num_matrices): - for j in range(num_rows): - for k in range(num_columns): - floats[num_rows * num_columns * i + num_columns * j + k] = ( - matrices[i][j][k]) - return floats - -cdef _compile_shader(GLuint shader, name, shader_type): - """ - Compiles given shader and prints compile errors - (using name and shader_type for formatting). - """ - - glCompileShader(shader) - - # The remaining code is just error checking - - cdef GLint status = GL_FALSE - glGetShaderiv(shader, GL_COMPILE_STATUS, &status) - - print_gl_errors("glCompileShader") - - if status == GL_TRUE: - return True - - print("Compiling %s shader %s failed." % (shader_type, name)) - - cdef GLchar * text = NULL - cdef GLint text_len - glGetShaderiv(shader, GL_INFO_LOG_LENGTH, &text_len) - if text_len > 0: - text = malloc(text_len) - glGetShaderInfoLog(shader, text_len, NULL, text) - print(text) - free(text) - - return False - -cdef _link_program(GLuint program, name): - """ - Links given program and prints linking errors - (using name for formatting). - """ - - glLinkProgram(program) - - print_gl_errors("glLinkProgram") - - # The remaining code is just for error checking - - cdef GLint status = GL_FALSE - glGetProgramiv(program, GL_LINK_STATUS, &status) - - if status == GL_TRUE: - return True - - print("Linking GLSL program '%s' failed." % name) - - cdef GLchar * text = NULL - cdef GLint text_len - glGetProgramiv(program, GL_INFO_LOG_LENGTH, &text_len) - if text_len > 0: - text = malloc(text_len) - glGetProgramInfoLog(program, text_len, NULL, text) - print(text) - free(text) - - return False - -cdef class UniformBufferObject: - cdef size_t _buffer_size - cdef char * _buffer - cdef GLuint _uniform_buffer_object - cdef _name_to_offset - - def __init__(self, buffer_size, name_to_offset): - self._buffer = NULL - self._buffer_size = 0 - self._uniform_buffer_object = 0 - - self._name_to_offset = name_to_offset - self._buffer_size = buffer_size - self._buffer = (malloc(self._buffer_size)) - - glGenBuffers(1, &self._uniform_buffer_object) - glBindBuffer(GL_UNIFORM_BUFFER, self._uniform_buffer_object) - glBufferData(GL_UNIFORM_BUFFER, - self._buffer_size, - NULL, - GL_STATIC_DRAW) - glBindBuffer(GL_UNIFORM_BUFFER, 0) - - print_gl_errors("UniformBufferObject.__init__") - - def set(self, name, uniform_type, value): - cdef size_t offset - cdef size_t l - cdef size_t i - cdef size_t j - cdef size_t k - cdef GLfloat * float_array - - offset = self._name_to_offset[name] - - if uniform_type == 'vec4[]': - l = len(value) - if offset + 4 * 4 * l > self._buffer_size: - raise Exception( - ("Data for %s of length %d at offset %d not fitting " - "into uniform buffer object of size %d") % ( - name, l, offset, self._buffer_size)) - - float_array = (self._buffer + offset) - for i in range(l): - for j in range(4): - float_array[4 * i + j] = value[i][j] - elif uniform_type == 'mat4[]': - l = len(value) - if offset + 4 * 4 * 4 * l > self._buffer_size: - raise Exception( - ("Data for %s of length %d at offset %d not fitting " - "into uniform buffer object of size %d") % ( - name, l, offset, self._buffer_size)) - - float_array = (self._buffer + offset) - for i in range(l): - for j in range(4): - for k in range(4): - float_array[4 * 4 * i + 4 * j + k] = value[i][j][k] - elif uniform_type == 'int[]': - l = len(value) - if offset + 16 * l > self._buffer_size: - raise Exception( - ("Data for %s of length %d at offset %d not fitting " - "into uniform buffer object of size %d") % ( - name, l, offset, self._buffer_size)) - - int_array = (self._buffer + offset) - for i in range(l): - int_array[4 * i] = value[i] - elif uniform_type == 'float[]': - l = len(value) - if offset + 16 * l > self._buffer_size: - raise Exception( - ("Data for %s of length %d at offset %d not fitting " - "into uniform buffer object of size %d") % ( - name, l, offset, self._buffer_size)) - - float_array = (self._buffer + offset) - for i in range(l): - float_array[4 * i] = value[i] - else: - raise Exception( - ("Unsupported uniform type %s for " - "uniform %s in uniform block") % ( - uniform_type, name)) - - def commit(self): - glBindBuffer(GL_UNIFORM_BUFFER, self._uniform_buffer_object) - glBufferData(GL_UNIFORM_BUFFER, - self._buffer_size, - self._buffer, - GL_STATIC_DRAW) - - def bind_block(self, GLuint binding_point): - glBindBufferBase(GL_UNIFORM_BUFFER, - binding_point, - self._uniform_buffer_object) - - def __dealloc__(self): - free(self._buffer) - self._buffer = NULL - - def delete_resource(self): - glDeleteBuffers(1, &self._uniform_buffer_object) - self._uniform_buffer_object = 0 - -cdef class GLSLProgram: - """ - A Cython class representing a GLSL program. Given GLSL source for a - vertex and fragment shader, a GLSLProgram object compiles the shaders - and links them to a GLSL program in the current GL context. - - To use the program for drawing, call the object's use_program method. - After use_program, you can bind uniform's by bind_uniforms. - """ - - cdef GLuint _vertex_shader - cdef GLuint _fragment_shader - cdef GLuint _glsl_program - cdef GLuint _width - cdef GLuint _height - cdef bint _is_valid - cdef _name_to_uniform_block - - def __init__(self, vertex_shader_source, fragment_shader_source, - uniform_block_names_sizes_and_offsets = [], - name = "unnamed"): - self._name_to_uniform_block = {} - - cdef const GLchar* c_vertex_shader_source = vertex_shader_source - cdef const GLchar* c_fragment_shader_source = fragment_shader_source - - clear_gl_errors() - - self._vertex_shader = glCreateShader(GL_VERTEX_SHADER) - self._fragment_shader = glCreateShader(GL_FRAGMENT_SHADER) - self._glsl_program = glCreateProgram() - glShaderSource(self._vertex_shader, - 1, &c_vertex_shader_source, NULL) - glShaderSource(self._fragment_shader, - 1, &c_fragment_shader_source, NULL) - self._is_valid = self._compile_and_link(name) - - if self._is_valid: - for block_name, block_size, offsets in ( - uniform_block_names_sizes_and_offsets): - self._name_to_uniform_block[block_name] = ( - UniformBufferObject(block_size, offsets)) - - print_gl_errors("GLSLProgram.__init__") - - if not self._is_valid: - # Only one client so far, so we can give a very concrete - # error message about what is most likely going on. - print("Most likely causes:") - print(" * The triangulation has too many tetrahedra") - print(" (given the number of uniforms your graphics card supports).") - print(" * Your graphics card does not support the required OpenGL version.") - print(" Required version Your version") - print(" OpenGL: 3.2 %s" % get_gl_string('GL_VERSION')) - print(" GLSL: 1.50 %s" % get_gl_string('GL_SHADING_LANGUAGE_VERSION')) - - if False: - if fragment_shader_source: - open('/tmp/fragment.glsl', 'wb').write(fragment_shader_source) - - def _compile_and_link(self, name): - if not _compile_shader(self._vertex_shader, name, 'vertex'): - return False - - if not _compile_shader(self._fragment_shader, name, 'fragment'): - return False - - glAttachShader(self._glsl_program, self._vertex_shader) - glAttachShader(self._glsl_program, self._fragment_shader) - - if not _link_program(self._glsl_program, name): - return False - - return True - - def is_valid(self): - return self._is_valid - - def bind_uniforms(self, name_to_type_and_value): - """ - Bind uniforms. This method can only be used after use_program - was called. It can be called several times (with different keys). - - The method takes a dictionary where the key is the name of the - uniform to bind and the value is a pair (type, py_value). - Here type is a string mimicking the GLSL type (e.g., int, vec2, - mat4, vec4[]). - - For mat4[], py_value needs to support len(py_value) - such that py_value[i][j][k] is convertible to a float and is used - for the entry (j,k) of the i-th matrix. - """ - - cdef size_t i - cdef size_t j - cdef size_t l - cdef GLint loc - cdef GLfloat * floats = NULL - cdef GLint * integers - cdef GLfloat mat4[16] - cdef size_t block_size - cdef GLuint block_index - - if not self._is_valid: - return - - clear_gl_errors() - - for name, (uniform_type, value) in name_to_type_and_value.items(): - name_parts = name.split('.') - uniform_name = name_parts[-1] - - if len(name_parts) == 1: - loc = glGetUniformLocation(self._glsl_program, - uniform_name.encode('ascii')) - if uniform_type == 'int': - glUniform1i(loc, int(value)) - elif uniform_type == 'float': - glUniform1f(loc, float(value)) - elif uniform_type == 'bool': - glUniform1i(loc, int(1 if value else 0)) - elif uniform_type == 'vec2': - glUniform2f(loc, float(value[0]), float(value[1])) - elif uniform_type == 'ivec2': - glUniform2i(loc, int(value[0]), int(value[1])) - elif uniform_type == 'mat4': - for i in range(4): - for j in range(4): - mat4[4*i + j] = value[i][j] - glUniformMatrix4fv(loc, 1, - 0, # transpose = false - mat4) - elif uniform_type == 'int[]': - l = len(value) - integers = malloc(l * sizeof(GLint)) - try: - for i in range(l): - integers[i] = value[i] - glUniform1iv(loc, l, integers) - finally: - free(integers) - elif uniform_type == 'float[]': - l = len(value) - floats = malloc(l * sizeof(GLfloat)) - try: - for i in range(l): - floats[i] = value[i] - glUniform1fv(loc, l, floats) - finally: - free(floats) - elif uniform_type == 'vec2[]': - l = len(value) - floats = malloc(2 * l * sizeof(GLfloat)) - try: - for i in range(l): - for j in range(2): - floats[2 * i + j] = value[i][j] - glUniform2fv(loc, l, floats) - finally: - free(floats) - elif uniform_type == 'vec3[]': - l = len(value) - floats = malloc(3 * l * sizeof(GLfloat)) - try: - for i in range(l): - for j in range(3): - floats[3 * i + j] = value[i][j] - glUniform3fv(loc, l, floats) - finally: - free(floats) - elif uniform_type == 'vec4[]': - l = len(value) - floats = malloc(4 * l * sizeof(GLfloat)) - try: - for i in range(l): - for j in range(4): - floats[4 * i + j] = value[i][j] - glUniform4fv(loc, l, floats) - finally: - free(floats) - elif uniform_type == 'mat2[]': - l = len(value) - try: - floats = _convert_matrices_to_floats( - value, l, 2, 2) - glUniformMatrix2fv(loc, l, - 0, # transpose = false - floats) - finally: - free(floats) - elif uniform_type == 'mat2x3[]': - l = len(value) - try: - floats = _convert_matrices_to_floats( - value, l, 2, 3) - glUniformMatrix2x3fv(loc, l, - 0, # transpose = false - floats) - finally: - free(floats) - elif uniform_type == 'mat3x2[]': - l = len(value) - try: - floats = _convert_matrices_to_floats( - value, l, 3, 2) - glUniformMatrix3x2fv(loc, l, - 0, # transpose = false - floats) - finally: - free(floats) - elif uniform_type == 'mat4[]': - l = len(value) - try: - floats = _convert_matrices_to_floats( - value, l, 4, 4) - glUniformMatrix4fv(loc, l, - 0, # transpose = false - floats) - finally: - free(floats) - else: - raise Exception( - "Unsupported uniform type %s" % uniform_type) - - print_gl_errors("uniform") - - else: - - block_name = name_parts[0] - self._name_to_uniform_block[block_name].set( - uniform_name, uniform_type, value) - - for i, (block_name, buffer_object) in enumerate( - self._name_to_uniform_block.items()): - block_index = glGetUniformBlockIndex( - self._glsl_program, block_name.encode('ascii')) - - if block_index != GL_INVALID_INDEX: - glUniformBlockBinding( - self._glsl_program, block_index, i) - - buffer_object.commit() - buffer_object.bind_block(i) - - print_gl_errors("uniform blocks") - - def use_program(self): - """ - Use program. Assumes that the current GL context is the context - in which the program was constructed. - """ - - glUseProgram(self._glsl_program) - - def delete_resource(self): - """ - Delete shaders associated to program. - Note that this happens implicitly when the GL context (widget) is - destroyed, but it won't happen when destroying the python object. - """ - - for uniform_block in self._name_to_uniform_block.values(): - uniform_block.delete_resource() - - glDeleteShader(self._vertex_shader) - glDeleteShader(self._fragment_shader) - glDeleteProgram(self._glsl_program) - - self._vertex_shader = 0 - self._fragment_shader = 0 - self._glsl_program = 0 - self._name_to_uniform_block = {} - -cdef class VertexBuffer: - """ - Encapsulates a vertex array and vertex buffer object. - - Data can be put into the vertex buffer object with load. - To bind the GL objects so that a shader can consume them (as vertex - attribute 0), call bind. - - For now, it only supports a single vertex buffer object holding - float 1-, 2-, 3-, or 4-vectors. In the future, we might - support having several vertex buffer objects. - """ - - # vertex array object - cdef GLuint _vao - - # Note: _vbo and _dimension would need to be an array - # if we were to support several vertex buffer objects - # Note: we would need to add GLEnum _type to remember the - # type if we support vertex buffer objects different from - # float. - - # vertex buffer - cdef GLuint _vbo - # Whether we loaded 1-, 2-, 3-, 4-vectors into buffer - cdef unsigned int _dimension - - def __cinit__(self): - # Set the indices to zero so that it is safe to call bind and - # delete on them when glGen... fails. - self._vao = 0 - self._vbo = 0 - - self._dimension = 4 - - clear_gl_errors() - - # Note that vertex array objects have some issues - # on Mac OS X. Checking for errors here. - - glGenVertexArrays(1, &self._vao) - print_gl_errors("glGenVertexArrays") - - glBindVertexArray(self._vao) - print_gl_errors("glBindVertexArray") - - glGenBuffers(1, &self._vbo) - print_gl_errors("glGenBuffers") - - def bind(self): - clear_gl_errors() - glBindVertexArray(self._vao) - print_gl_errors("glBindVertexArray") - - glBindBuffer(GL_ARRAY_BUFFER, self._vbo) - - # Use that vertex buffer as vertex attribute 0 - # (i.e., the shader's first "in vec4" will be fed by the - # buffer). - glEnableVertexAttribArray(0) - print_gl_errors("glEnableVertexAttribArray") - - # Specify that the buffer is interpreted as pairs of floats (x,y) - # GLSL will complete this to (x,y,0,1) - glVertexAttribPointer(0, self._dimension, - GL_FLOAT, GL_FALSE, - sizeof(GLfloat) * self._dimension, - NULL) - print_gl_errors("glVertexAttribPointer") - - def load(self, vertex_data): - """ - Load data into GL vertex buffer objects. - vertex_data needs to be something like - [[0,0,0],[1,1,0],[0,1,1],[1,0,1]]. - """ - - cdef unsigned int i - cdef unsigned int j - - self._dimension = len(vertex_data[0]) - - cdef size_t num_bytes = ( - sizeof(GLfloat) * self._dimension * len(vertex_data)) - - cdef GLfloat * verts = malloc(num_bytes) - try: - for i, vertex in enumerate(vertex_data): - for j in range(self._dimension): - verts[self._dimension * i + j] = vertex[j] - - clear_gl_errors() - # This is not really necessary to push data to - # the GL_ARRAY_BUFFER. - glBindVertexArray(self._vao) - print_gl_errors("glBindVertexArray") - - glBindBuffer(GL_ARRAY_BUFFER, self._vbo) - - glBufferData(GL_ARRAY_BUFFER, - num_bytes, - verts, - GL_STATIC_DRAW) - - finally: - free(verts) - - def delete_resource(self): - # Same comments as for GLSLProgram.delete_resource apply - - glDeleteBuffers(1, &self._vbo) - glDeleteVertexArrays(1, &self._vao) - - self._vbo = 0 - self._vao = 0 - -class SimpleImageShaderWidget(RawOpenGLWidget): - """ - An image shader is a GLSL program that does all of its computation in - the fragment shader. - - This widget displays an image generated by an image - shader. The draw method simply draws a triangle covering the - entire screen, which causes the fragment shader to be run on - every pixel in the window. The fragment shader source and - - optionally - sizes and offsets for uniform buffer blocks can - be set with set_fragment_shader_source. - - The uniform vec2 viewportSize contains the view port size. - """ - - profile = '3_2' - - vertex_shader_source = b""" - #version 150 - - // Note that GLSL ES 3.0 is based on GLSL 3.30 and used for WebGL 2.0. - // GLSL 1.50 came with OpenGL 3.2. - - in vec4 position; - - void main() - { - gl_Position = position; // no-op - } - - """ - - fragment_shader_source = b""" - #version 150 - - out vec4 out_FragColor; - - void main() - { - out_FragColor = vec4(1.0, 0.0, 0.0, 1.0); - } - """ - - def __init__(self, master, - **kw): - RawOpenGLWidget.__init__(self, master, **kw) - - self._vertex_buffer = VertexBuffer() - self._vertex_buffer.load(((3,-1), (-1,3), (-1,-1))) - - self.image_shader = GLSLProgram( - self.vertex_shader_source, - self.fragment_shader_source, - name = "fallback image shader") - self.textures = [] - self.report_time_callback = None - - def set_textures(self, texture_files): - self.make_current() - - for texture in self.textures: - texture.delete_resource() - - self.textures = [] - for texture_file in texture_files: - texture = None - try: - texture = ImageBasedTexture(texture_file) - except Exception as e: - print("Warning could not read texture %s" % texture_file) - print(e) - - self.textures.append(texture) - - def set_fragment_shader_source(self, - source, - uniform_block_names_sizes_and_offsets = []): - self.image_shader.delete_resource() - self.image_shader = GLSLProgram( - self.vertex_shader_source, - source, - uniform_block_names_sizes_and_offsets = uniform_block_names_sizes_and_offsets, - name = "image shader") - - def render_to_array(self, width, height, as_float = False): - """ - Renders the image into an off-screen framebuffer - of given width and height and returns the result as an array. - - The array either holds unsigned byte or float RGB. - """ - - cdef GLuint fbo - cdef GLenum color_texture_type - cdef GLuint color_texture - cdef GLuint depth_texture - cdef array_type - cdef array.array c_array - - self.make_current() - - if as_float: - array_type = 'f' - color_texture_type = GL_FLOAT - else: - array_type = 'B' - color_texture_type = GL_UNSIGNED_BYTE - - # Create texture for color attachment - glGenTextures(1, &color_texture) - glBindTexture(GL_TEXTURE_2D, color_texture) - glTexImage2D(GL_TEXTURE_2D, 0, - GL_RGB, width, height, - 0, - GL_RGB, - color_texture_type, - NULL) - glTexParameteri(GL_TEXTURE_2D, - GL_TEXTURE_MIN_FILTER, - GL_LINEAR) - glTexParameteri(GL_TEXTURE_2D, - GL_TEXTURE_MAG_FILTER, - GL_LINEAR) - - # Create texture for depth attachment - glGenTextures(1, &depth_texture) - glBindTexture(GL_TEXTURE_2D, depth_texture) - glTexImage2D(GL_TEXTURE_2D, 0, - GL_DEPTH_COMPONENT, width, height, - 0, - GL_DEPTH_COMPONENT, - GL_FLOAT, - NULL) - glTexParameteri(GL_TEXTURE_2D, - GL_TEXTURE_MIN_FILTER, - GL_LINEAR) - glTexParameteri(GL_TEXTURE_2D, - GL_TEXTURE_MAG_FILTER, - GL_LINEAR) - - # Create framebuffer - glGenFramebuffers(1, &fbo) - glBindFramebuffer(GL_FRAMEBUFFER, fbo) - glFramebufferTexture2D(GL_FRAMEBUFFER, - GL_COLOR_ATTACHMENT0, - GL_TEXTURE_2D, - color_texture, 0) - glFramebufferTexture2D(GL_FRAMEBUFFER, - GL_DEPTH_ATTACHMENT, - GL_TEXTURE_2D, - depth_texture, 0) - if glCheckFramebufferStatus(GL_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE: - raise Exception("Incomplete framebuffer") - - # Render into the framebuffer - self.redraw(width, height, - skip_swap_buffers = True) - glFinish() - - # Allocate memory and read framebuffer into it - c_array = array.array(array_type) - array.resize(c_array, 3 * width * height) - glReadPixels(0, 0, width, height, - GL_RGB, - color_texture_type, - c_array.data.as_voidptr) - - # Unbind framebuffer so that stuff is rendered - # to screen again. - glBindFramebuffer(GL_FRAMEBUFFER, 0) - - glDeleteFramebuffers(1, &fbo) - glDeleteTextures(1, &color_texture) - glDeleteTextures(1, &depth_texture) - - print_gl_errors("Render to off-screen area") - - return c_array - - def save_image(self, width, height, outfile): - """ - Writes image of given width and height - as png to given outfile (file object returned by, - .e.g, open("myFile.png", "wb")). - """ - - data = self.render_to_array(width, height) - - # Png writer expects row - we also need to - # flip the image vertically. - stride = 3 * width - rows = [ data[i * stride : (i+1) * stride] - for i in range(height - 1, -1, -1) ] - - writer = png.Writer( - width, height, - greyscale = False, - bitdepth = 8, - alpha = False) - writer.write(outfile, rows) - - def read_depth_value(self, x, y): - - cdef GLfloat depth - - width = self.winfo_width() - height = self.winfo_height() - - self.make_current() - - self.redraw(width, height, - skip_swap_buffers = True, - include_depth_value = True) - glFinish() - - glReadPixels(x, height - y, 1, 1, - GL_DEPTH_COMPONENT, - GL_FLOAT, &depth) - - return (depth, width, height) - - def redraw(self, width, height, - skip_swap_buffers = False, - include_depth_value = False): - - if self.report_time_callback: - start_time = time.time() - - glViewport(0, 0, width, height) - if include_depth_value: - # Writes to z-buffer are only done when GL_DEPTH_TEST - # is enabled - glClear(GL_DEPTH_BUFFER_BIT) - glEnable(GL_DEPTH_TEST) - else: - glDisable(GL_DEPTH_TEST) - glDisable(GL_BLEND) - glDisable(GL_CULL_FACE) - - if self.image_shader.is_valid(): - for i, texture in enumerate(self.textures): - if texture: - glActiveTexture(GL_TEXTURE0 + i) - texture.bind() - - self.image_shader.use_program() - self.image_shader.bind_uniforms( - self.get_uniform_bindings(width, height)) - self._vertex_buffer.bind() - - glDrawArrays(GL_TRIANGLES, 0, 3) - - for i, texture in enumerate(self.textures): - if texture: - glActiveTexture(GL_TEXTURE0 + i) - texture.unbind() - glActiveTexture(GL_TEXTURE0) - - if self.report_time_callback: - glFinish() - self.report_time_callback(time.time() - start_time) - - if not skip_swap_buffers: - self.swap_buffers() - - def get_uniform_bindings(self, view_width, view_height): - return { - 'viewportSize' : ('vec2', (view_width, view_height)) - } - -# Module-level utilities for handling 4x4 matrices, represented as -# 1-dimensional arrays in column-major order (M[i,j] = A[i + 4*j]). - -cdef mat4_multiply(GLfloat *left, GLfloat *right, GLfloat *result): - """ - Multiply two 4x4 matrices represented as 1-dimensional arrays in - column-major order. If the result matrix is equal to either of - the operands, the multiplication will be done in place. - """ - cdef GLfloat temp[16] - cdef GLfloat *product = result - if result == right or result == left: - product = temp - cdef int i, j, k - for i in range(4): - for j in range(0, 16, 4): - product[i + j] = 0 - for k in range(4): - product[i + j] += left[i + 4*k] * right[k + j] - if product == temp: - for i in range(16): - result[i] = temp[i] - -cdef inline mat4_set_to_identity(GLfloat *matrix): - """ - Set a 4x4 matrix to the identity. - """ - cdef int i, j - for i in range(4): - for j in range(4): - matrix[i + 4*j] = 1.0 if i == j else 0.0 - -cdef class GLSLPerspectiveView: - """ - Mixin class to create a perspective view using GLSL. An object of - this class maintains a model view matrix, a projection matrix and the - product of the two. These are made available to the shaders by - get_uniform_bindings. - """ - # Rotates about a line through the origin. - cdef GLfloat _rotation[16] - # Translates center to origin, rotates, then translates into view. - cdef GLfloat _model_view[16] - # Maps the perspective frustrum to the standard cube. - cdef GLfloat _projection[16] - # Combined transformation, passed to the shader as uniform data. - cdef GLfloat _mvp[16] - # Parameters to control the perspective view and the position of - # the model relative to the visible frustrum. These are exposed - # as properties. - cdef GLfloat _vertical_fov, _near, _far, _distance - cdef GLfloat _center[3] - - def __cinit__(self): - self._vertical_fov = 30.0 - self._near = 1.0 - self._far = 100.0 - self._distance = 10.0 - self._center = [0.0, 0.0, 0.0] - mat4_set_to_identity(self._rotation) - mat4_set_to_identity(self._model_view) - mat4_set_to_identity(self._projection) - - @property - def vertical_fov(self): - return self._vertical_fov - @vertical_fov.setter - def vertical_fov(self, GLfloat value): - self._vertical_fov = value - - @property - def near(self): - return self._near - @near.setter - def near(self, GLfloat value): - self._near = value - - @property - def far(self): - return self._far - @far.setter - def far(self, GLfloat value): - self._far = value - - @property - def distance(self): - return self._distance - @distance.setter - def distance(self, GLfloat value): - self._distance = value - - @property - def center(self): - cdef int i - return [self._center[i] for i in range(3)] - @center.setter - def center(self, vector): - cdef int i - for i in range(3): - self._center[i] = vector[i] - - cdef compute_mvp(self, width, height): - """ - First compute the so-called projection matrix, which is actually - the matrix of an orientation reversing affine transformation. - Assume that 0 < n < f and consider the rectangular cone in R^3 - which has its apex at the origin and is centered on the negative - z-axis. The vertical angle of the cone, i.e. the vertical field - of view, is given in degrees by the vertical_fov attribute of this - object. The region which is visible in the perspective view is - the frustrum of this cone consisting of points which lie between - the "near plane" z = -self.near and the "far plane" z = -self.far. - Everything outside of ths frustrum is clipped away. - - The rectangular faces of the frustrum which lie respectively in - the near and far plane are called the near and far rectangles. By - the standard cube we mean the cube with vertices (+-1, +-1, - +-1). The affine map represented by the projection matrix maps the - near rectangle to the bottom face of the standard cube and maps - the far rectangle to the top face of the standard cube. The - orientations of the x and y axes are preserved while the - orientation of the z-axis is reversed. - - While the (non-singular) projection matrix is obviously not a - projection in the sense of linear algebra, after the vertex shader - computes the locations of all vertices, GL automatically clips to - the standard cube, projects to the xy-plane and then applies an - affine map which sends the image of the cube onto the viewport - rectangle. If the vertex shader applies this affine map to each - input vertex location, the effect is to render the objects inside - the frustrum in perspective. - - Finally, compute the product of the projection matrix, the - translation matrix (which translates the model center to a point - on the negative z-axis) and the rotation matrix. - """ - cdef GLfloat ymax = self._near * tan(self._vertical_fov *pi/360.0) - cdef GLfloat aspect = float(width)/float(height) - cdef GLfloat xmax = ymax * aspect - cdef GLfloat n = self._near, f = self._far - cdef GLfloat *M = self._projection - # Fill in the entries of the "projection" in column-major order. - M[0] = n/xmax; M[1] = M[2] = M[3] = 0.0 - M[4] = 0; M[5] = n/ymax; M[6] = M[7] = 0.0 - M[8] = M[9] = 0.0; M[10] = -(f + n)/(f - n); M[11] = -1.0 - M[12] = M[13] = 0.0; M[14] = -2.0*n*f/(f - n); M[15] = 0.0 - # Construct the model view matrix. - mat4_set_to_identity(self._model_view) - self.translate(-self._center[0], -self._center[1], -self._center[2]) - mat4_multiply(self._rotation, self._model_view, self._model_view) - self.translate(0, 0, -self._distance) - # Construct the MVP matrix. - mat4_multiply(self._projection, self._model_view, self._mvp) - - cpdef translate(self, GLfloat x, GLfloat y, GLfloat z): - """ - Multiply the model view matrix by a translation matrix, without - doing unnecessary arithmetic. - """ - cdef int i - cdef GLfloat a - cdef GLfloat *M = self._model_view - for i in range(0,16,4): - a = M[i+3] - M[i] += x*a - M[i+1] += y*a - M[i+2] += z*a - - cpdef rotate(self, GLfloat theta, GLfloat x, GLfloat y, GLfloat z): - """ - Update self._rotation by multiplying by a rotation matrix with - angle theta and axis given by a unit vector . The caller - is responsible for normalizing the axial vector. - """ - # 1 - cos(theta) = 2*haversine(theta) - cdef GLfloat c = cos(theta), s = sin(theta), h = 1 - c - cdef GLfloat xs = x*s, ys = y*s, zs = z*s - cdef GLfloat xx = x*x, xh = x*h, xxh = xx*h, xyh = y*xh, xzh = z*xh - cdef GLfloat yy = y*y, yh = y*h, yyh = yy*h, yzh = z*yh, zzh = z*z*h - cdef GLfloat rot[16] - # entries in column-major order - rot = (xxh + c, xyh - zs, xzh + ys, 0, - xyh + zs, yyh + c, yzh - xs, 0, - xzh - ys, yzh + xs, zzh + c, 0, - 0, 0, 0, 1.0) - mat4_multiply(rot, self._rotation, self._rotation) - - def get_uniform_bindings(self, view_width, view_height): - self.compute_mvp(view_width, view_height) - - def to_py(m): - return [ [ float(m[4 * i + j]) for j in range(4) ] - for i in range(4) ] - - return { - 'MVPMatrix': ('mat4', to_py(self._mvp)), - 'ModelViewMatrix': ('mat4', to_py(self._model_view)), - 'ProjectionMatrix': ('mat4', to_py(self._projection)) } - -class GLSLPerspectiveWidget(RawOpenGLWidget, GLSLPerspectiveView): - """ - A widget which renders a collection of OpenGL objects in perspective, - using a GLSL vertex shader to compute the projection. - """ - profile = '3_2' - - def __init__(self, master, cnf={}, **kw): - RawOpenGLWidget.__init__(self, master, cnf={}, **kw) - GLSLPerspectiveView.__init__(self) - self.make_current() - self.objects = [] - glDisable(GL_CULL_FACE) - - def add_object(self, obj): - self.objects.append(obj) - - def redraw(self, width, height, skip_swap_buffers = False): - glViewport(0, 0, width, height) - glClearColor(0.0, 0.0, 0.0, 1.0); - glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); - for object in self.objects: - object.draw(width, height) - if not skip_swap_buffers: - self.swap_buffers() - +include "modern/data_based_texture.pyx" +include "modern/image_based_texture.pyx" +include "modern/uniform_buffer_object.pyx" +include "modern/glsl_program.pyx" +include "modern/vertex_buffer.pyx" +include "modern/simple_image_shader_widget.pyx" +include "modern/glsl_perspective_view.pyx" +include "modern/glsl_perspective_widget.pyx" include "modern/triangle.pyx" diff --git a/opengl/modern/data_based_texture.pyx b/opengl/modern/data_based_texture.pyx new file mode 100644 index 00000000..b6de69b9 --- /dev/null +++ b/opengl/modern/data_based_texture.pyx @@ -0,0 +1,45 @@ +cdef class DataBasedTexture: + cdef GLuint _textureName + + def __cinit__(self): + self._textureName = 0 + + def __init__(self, + unsigned int width, + unsigned int height, + unsigned char[:] rgba_data): + + if rgba_data.size != 4 * width * height: + raise RuntimeError("Length of rgba_data not matching") + + glGenTextures(1, &self._textureName) + glActiveTexture(GL_TEXTURE0) + glBindTexture(GL_TEXTURE_2D, self._textureName) + + glTexImage2D(GL_TEXTURE_2D, 0, + GL_RGBA, + width, + height, + 0, + GL_RGBA, + GL_UNSIGNED_BYTE, + &rgba_data[0]) + + glTexParameteri(GL_TEXTURE_2D, + GL_TEXTURE_MIN_FILTER, + GL_LINEAR) + glTexParameteri(GL_TEXTURE_2D, + GL_TEXTURE_MAG_FILTER, + GL_LINEAR) + + glBindTexture(GL_TEXTURE_2D, 0) + + def bind(self): + glBindTexture(GL_TEXTURE_2D, self._textureName) + + def unbind(self): + glBindTexture(GL_TEXTURE_2D, 0) + + def delete_resource(self): + glDeleteTextures(1, &self._textureName) + self._textureName = 0 diff --git a/opengl/modern/glsl_perspective_view.pyx b/opengl/modern/glsl_perspective_view.pyx new file mode 100644 index 00000000..5e0215ce --- /dev/null +++ b/opengl/modern/glsl_perspective_view.pyx @@ -0,0 +1,199 @@ + +# Module-level utilities for handling 4x4 matrices, represented as +# 1-dimensional arrays in column-major order (M[i,j] = A[i + 4*j]). + +cdef mat4_multiply(GLfloat *left, GLfloat *right, GLfloat *result): + """ + Multiply two 4x4 matrices represented as 1-dimensional arrays in + column-major order. If the result matrix is equal to either of + the operands, the multiplication will be done in place. + """ + cdef GLfloat temp[16] + cdef GLfloat *product = result + if result == right or result == left: + product = temp + cdef int i, j, k + for i in range(4): + for j in range(0, 16, 4): + product[i + j] = 0 + for k in range(4): + product[i + j] += left[i + 4*k] * right[k + j] + if product == temp: + for i in range(16): + result[i] = temp[i] + +cdef inline mat4_set_to_identity(GLfloat *matrix): + """ + Set a 4x4 matrix to the identity. + """ + cdef int i, j + for i in range(4): + for j in range(4): + matrix[i + 4*j] = 1.0 if i == j else 0.0 + +cdef class GLSLPerspectiveView: + """ + Mixin class to create a perspective view using GLSL. An object of + this class maintains a model view matrix, a projection matrix and the + product of the two. These are made available to the shaders by + get_uniform_bindings. + """ + # Rotates about a line through the origin. + cdef GLfloat _rotation[16] + # Translates center to origin, rotates, then translates into view. + cdef GLfloat _model_view[16] + # Maps the perspective frustrum to the standard cube. + cdef GLfloat _projection[16] + # Combined transformation, passed to the shader as uniform data. + cdef GLfloat _mvp[16] + # Parameters to control the perspective view and the position of + # the model relative to the visible frustrum. These are exposed + # as properties. + cdef GLfloat _vertical_fov, _near, _far, _distance + cdef GLfloat _center[3] + + def __cinit__(self): + self._vertical_fov = 30.0 + self._near = 1.0 + self._far = 100.0 + self._distance = 10.0 + self._center = [0.0, 0.0, 0.0] + mat4_set_to_identity(self._rotation) + mat4_set_to_identity(self._model_view) + mat4_set_to_identity(self._projection) + + @property + def vertical_fov(self): + return self._vertical_fov + @vertical_fov.setter + def vertical_fov(self, GLfloat value): + self._vertical_fov = value + + @property + def near(self): + return self._near + @near.setter + def near(self, GLfloat value): + self._near = value + + @property + def far(self): + return self._far + @far.setter + def far(self, GLfloat value): + self._far = value + + @property + def distance(self): + return self._distance + @distance.setter + def distance(self, GLfloat value): + self._distance = value + + @property + def center(self): + cdef int i + return [self._center[i] for i in range(3)] + @center.setter + def center(self, vector): + cdef int i + for i in range(3): + self._center[i] = vector[i] + + cdef compute_mvp(self, width, height): + """ + First compute the so-called projection matrix, which is actually + the matrix of an orientation reversing affine transformation. + Assume that 0 < n < f and consider the rectangular cone in R^3 + which has its apex at the origin and is centered on the negative + z-axis. The vertical angle of the cone, i.e. the vertical field + of view, is given in degrees by the vertical_fov attribute of this + object. The region which is visible in the perspective view is + the frustrum of this cone consisting of points which lie between + the "near plane" z = -self.near and the "far plane" z = -self.far. + Everything outside of ths frustrum is clipped away. + + The rectangular faces of the frustrum which lie respectively in + the near and far plane are called the near and far rectangles. By + the standard cube we mean the cube with vertices (+-1, +-1, + +-1). The affine map represented by the projection matrix maps the + near rectangle to the bottom face of the standard cube and maps + the far rectangle to the top face of the standard cube. The + orientations of the x and y axes are preserved while the + orientation of the z-axis is reversed. + + While the (non-singular) projection matrix is obviously not a + projection in the sense of linear algebra, after the vertex shader + computes the locations of all vertices, GL automatically clips to + the standard cube, projects to the xy-plane and then applies an + affine map which sends the image of the cube onto the viewport + rectangle. If the vertex shader applies this affine map to each + input vertex location, the effect is to render the objects inside + the frustrum in perspective. + + Finally, compute the product of the projection matrix, the + translation matrix (which translates the model center to a point + on the negative z-axis) and the rotation matrix. + """ + cdef GLfloat ymax = self._near * tan(self._vertical_fov *pi/360.0) + cdef GLfloat aspect = float(width)/float(height) + cdef GLfloat xmax = ymax * aspect + cdef GLfloat n = self._near, f = self._far + cdef GLfloat *M = self._projection + # Fill in the entries of the "projection" in column-major order. + M[0] = n/xmax; M[1] = M[2] = M[3] = 0.0 + M[4] = 0; M[5] = n/ymax; M[6] = M[7] = 0.0 + M[8] = M[9] = 0.0; M[10] = -(f + n)/(f - n); M[11] = -1.0 + M[12] = M[13] = 0.0; M[14] = -2.0*n*f/(f - n); M[15] = 0.0 + # Construct the model view matrix. + mat4_set_to_identity(self._model_view) + self.translate(-self._center[0], -self._center[1], -self._center[2]) + mat4_multiply(self._rotation, self._model_view, self._model_view) + self.translate(0, 0, -self._distance) + # Construct the MVP matrix. + mat4_multiply(self._projection, self._model_view, self._mvp) + + cpdef translate(self, GLfloat x, GLfloat y, GLfloat z): + """ + Multiply the model view matrix by a translation matrix, without + doing unnecessary arithmetic. + """ + cdef int i + cdef GLfloat a + cdef GLfloat *M = self._model_view + for i in range(0,16,4): + a = M[i+3] + M[i] += x*a + M[i+1] += y*a + M[i+2] += z*a + + cpdef rotate(self, GLfloat theta, GLfloat x, GLfloat y, GLfloat z): + """ + Update self._rotation by multiplying by a rotation matrix with + angle theta and axis given by a unit vector . The caller + is responsible for normalizing the axial vector. + """ + # 1 - cos(theta) = 2*haversine(theta) + cdef GLfloat c = cos(theta), s = sin(theta), h = 1 - c + cdef GLfloat xs = x*s, ys = y*s, zs = z*s + cdef GLfloat xx = x*x, xh = x*h, xxh = xx*h, xyh = y*xh, xzh = z*xh + cdef GLfloat yy = y*y, yh = y*h, yyh = yy*h, yzh = z*yh, zzh = z*z*h + cdef GLfloat rot[16] + # entries in column-major order + rot = (xxh + c, xyh - zs, xzh + ys, 0, + xyh + zs, yyh + c, yzh - xs, 0, + xzh - ys, yzh + xs, zzh + c, 0, + 0, 0, 0, 1.0) + mat4_multiply(rot, self._rotation, self._rotation) + + def get_uniform_bindings(self, view_width, view_height): + self.compute_mvp(view_width, view_height) + + def to_py(m): + return [ [ float(m[4 * i + j]) for j in range(4) ] + for i in range(4) ] + + return { + 'MVPMatrix': ('mat4', to_py(self._mvp)), + 'ModelViewMatrix': ('mat4', to_py(self._model_view)), + 'ProjectionMatrix': ('mat4', to_py(self._projection)) } diff --git a/opengl/modern/glsl_perspective_widget.pyx b/opengl/modern/glsl_perspective_widget.pyx new file mode 100644 index 00000000..3a7f43fe --- /dev/null +++ b/opengl/modern/glsl_perspective_widget.pyx @@ -0,0 +1,26 @@ +class GLSLPerspectiveWidget(RawOpenGLWidget, GLSLPerspectiveView): + """ + A widget which renders a collection of OpenGL objects in perspective, + using a GLSL vertex shader to compute the projection. + """ + profile = '3_2' + + def __init__(self, master, cnf={}, **kw): + RawOpenGLWidget.__init__(self, master, cnf={}, **kw) + GLSLPerspectiveView.__init__(self) + self.make_current() + self.objects = [] + glDisable(GL_CULL_FACE) + + def add_object(self, obj): + self.objects.append(obj) + + def redraw(self, width, height, skip_swap_buffers = False): + glViewport(0, 0, width, height) + glClearColor(0.0, 0.0, 0.0, 1.0); + glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); + for object in self.objects: + object.draw(width, height) + if not skip_swap_buffers: + self.swap_buffers() + diff --git a/opengl/modern/glsl_program.pyx b/opengl/modern/glsl_program.pyx new file mode 100644 index 00000000..1d0b5d8b --- /dev/null +++ b/opengl/modern/glsl_program.pyx @@ -0,0 +1,334 @@ +cdef _compile_shader(GLuint shader, name, shader_type): + """ + Compiles given shader and prints compile errors + (using name and shader_type for formatting). + """ + + glCompileShader(shader) + + # The remaining code is just error checking + + cdef GLint status = GL_FALSE + glGetShaderiv(shader, GL_COMPILE_STATUS, &status) + + print_gl_errors("glCompileShader") + + if status == GL_TRUE: + return True + + print("Compiling %s shader %s failed." % (shader_type, name)) + + cdef GLchar * text = NULL + cdef GLint text_len + glGetShaderiv(shader, GL_INFO_LOG_LENGTH, &text_len) + if text_len > 0: + text = malloc(text_len) + glGetShaderInfoLog(shader, text_len, NULL, text) + print(text) + free(text) + + return False + +cdef _link_program(GLuint program, name): + """ + Links given program and prints linking errors + (using name for formatting). + """ + + glLinkProgram(program) + + print_gl_errors("glLinkProgram") + + # The remaining code is just for error checking + + cdef GLint status = GL_FALSE + glGetProgramiv(program, GL_LINK_STATUS, &status) + + if status == GL_TRUE: + return True + + print("Linking GLSL program '%s' failed." % name) + + cdef GLchar * text = NULL + cdef GLint text_len + glGetProgramiv(program, GL_INFO_LOG_LENGTH, &text_len) + if text_len > 0: + text = malloc(text_len) + glGetProgramInfoLog(program, text_len, NULL, text) + print(text) + free(text) + + return False + +cdef class GLSLProgram: + """ + A Cython class representing a GLSL program. Given GLSL source for a + vertex and fragment shader, a GLSLProgram object compiles the shaders + and links them to a GLSL program in the current GL context. + + To use the program for drawing, call the object's use_program method. + After use_program, you can bind uniform's by bind_uniforms. + """ + + cdef GLuint _vertex_shader + cdef GLuint _fragment_shader + cdef GLuint _glsl_program + cdef GLuint _width + cdef GLuint _height + cdef bint _is_valid + cdef _name_to_uniform_block + + def __init__(self, vertex_shader_source, fragment_shader_source, + uniform_block_names_sizes_and_offsets = [], + name = "unnamed"): + self._name_to_uniform_block = {} + + cdef const GLchar* c_vertex_shader_source = vertex_shader_source + cdef const GLchar* c_fragment_shader_source = fragment_shader_source + + clear_gl_errors() + + self._vertex_shader = glCreateShader(GL_VERTEX_SHADER) + self._fragment_shader = glCreateShader(GL_FRAGMENT_SHADER) + self._glsl_program = glCreateProgram() + glShaderSource(self._vertex_shader, + 1, &c_vertex_shader_source, NULL) + glShaderSource(self._fragment_shader, + 1, &c_fragment_shader_source, NULL) + self._is_valid = self._compile_and_link(name) + + if self._is_valid: + for block_name, block_size, offsets in ( + uniform_block_names_sizes_and_offsets): + self._name_to_uniform_block[block_name] = ( + UniformBufferObject(block_size, offsets)) + + print_gl_errors("GLSLProgram.__init__") + + if not self._is_valid: + # Only one client so far, so we can give a very concrete + # error message about what is most likely going on. + print("Most likely causes:") + print(" * The triangulation has too many tetrahedra") + print(" (given the number of uniforms your graphics card supports).") + print(" * Your graphics card does not support the required OpenGL version.") + print(" Required version Your version") + print(" OpenGL: 3.2 %s" % get_gl_string('GL_VERSION')) + print(" GLSL: 1.50 %s" % get_gl_string('GL_SHADING_LANGUAGE_VERSION')) + + if False: + if fragment_shader_source: + open('/tmp/fragment.glsl', 'wb').write(fragment_shader_source) + + def _compile_and_link(self, name): + if not _compile_shader(self._vertex_shader, name, 'vertex'): + return False + + if not _compile_shader(self._fragment_shader, name, 'fragment'): + return False + + glAttachShader(self._glsl_program, self._vertex_shader) + glAttachShader(self._glsl_program, self._fragment_shader) + + if not _link_program(self._glsl_program, name): + return False + + return True + + def is_valid(self): + return self._is_valid + + def bind_uniforms(self, name_to_type_and_value): + """ + Bind uniforms. This method can only be used after use_program + was called. It can be called several times (with different keys). + + The method takes a dictionary where the key is the name of the + uniform to bind and the value is a pair (type, py_value). + Here type is a string mimicking the GLSL type (e.g., int, vec2, + mat4, vec4[]). + + For mat4[], py_value needs to support len(py_value) + such that py_value[i][j][k] is convertible to a float and is used + for the entry (j,k) of the i-th matrix. + """ + + cdef size_t i + cdef size_t j + cdef size_t l + cdef GLint loc + cdef GLfloat * floats = NULL + cdef GLint * integers + cdef GLfloat mat4[16] + cdef size_t block_size + cdef GLuint block_index + + if not self._is_valid: + return + + clear_gl_errors() + + for name, (uniform_type, value) in name_to_type_and_value.items(): + name_parts = name.split('.') + uniform_name = name_parts[-1] + + if len(name_parts) == 1: + loc = glGetUniformLocation(self._glsl_program, + uniform_name.encode('ascii')) + if uniform_type == 'int': + glUniform1i(loc, int(value)) + elif uniform_type == 'float': + glUniform1f(loc, float(value)) + elif uniform_type == 'bool': + glUniform1i(loc, int(1 if value else 0)) + elif uniform_type == 'vec2': + glUniform2f(loc, float(value[0]), float(value[1])) + elif uniform_type == 'ivec2': + glUniform2i(loc, int(value[0]), int(value[1])) + elif uniform_type == 'mat4': + for i in range(4): + for j in range(4): + mat4[4*i + j] = value[i][j] + glUniformMatrix4fv(loc, 1, + 0, # transpose = false + mat4) + elif uniform_type == 'int[]': + l = len(value) + integers = malloc(l * sizeof(GLint)) + try: + for i in range(l): + integers[i] = value[i] + glUniform1iv(loc, l, integers) + finally: + free(integers) + elif uniform_type == 'float[]': + l = len(value) + floats = malloc(l * sizeof(GLfloat)) + try: + for i in range(l): + floats[i] = value[i] + glUniform1fv(loc, l, floats) + finally: + free(floats) + elif uniform_type == 'vec2[]': + l = len(value) + floats = malloc(2 * l * sizeof(GLfloat)) + try: + for i in range(l): + for j in range(2): + floats[2 * i + j] = value[i][j] + glUniform2fv(loc, l, floats) + finally: + free(floats) + elif uniform_type == 'vec3[]': + l = len(value) + floats = malloc(3 * l * sizeof(GLfloat)) + try: + for i in range(l): + for j in range(3): + floats[3 * i + j] = value[i][j] + glUniform3fv(loc, l, floats) + finally: + free(floats) + elif uniform_type == 'vec4[]': + l = len(value) + floats = malloc(4 * l * sizeof(GLfloat)) + try: + for i in range(l): + for j in range(4): + floats[4 * i + j] = value[i][j] + glUniform4fv(loc, l, floats) + finally: + free(floats) + elif uniform_type == 'mat2[]': + l = len(value) + try: + floats = _convert_matrices_to_floats( + value, l, 2, 2) + glUniformMatrix2fv(loc, l, + 0, # transpose = false + floats) + finally: + free(floats) + elif uniform_type == 'mat2x3[]': + l = len(value) + try: + floats = _convert_matrices_to_floats( + value, l, 2, 3) + glUniformMatrix2x3fv(loc, l, + 0, # transpose = false + floats) + finally: + free(floats) + elif uniform_type == 'mat3x2[]': + l = len(value) + try: + floats = _convert_matrices_to_floats( + value, l, 3, 2) + glUniformMatrix3x2fv(loc, l, + 0, # transpose = false + floats) + finally: + free(floats) + elif uniform_type == 'mat4[]': + l = len(value) + try: + floats = _convert_matrices_to_floats( + value, l, 4, 4) + glUniformMatrix4fv(loc, l, + 0, # transpose = false + floats) + finally: + free(floats) + else: + raise Exception( + "Unsupported uniform type %s" % uniform_type) + + print_gl_errors("uniform") + + else: + + block_name = name_parts[0] + self._name_to_uniform_block[block_name].set( + uniform_name, uniform_type, value) + + for i, (block_name, buffer_object) in enumerate( + self._name_to_uniform_block.items()): + block_index = glGetUniformBlockIndex( + self._glsl_program, block_name.encode('ascii')) + + if block_index != GL_INVALID_INDEX: + glUniformBlockBinding( + self._glsl_program, block_index, i) + + buffer_object.commit() + buffer_object.bind_block(i) + + print_gl_errors("uniform blocks") + + def use_program(self): + """ + Use program. Assumes that the current GL context is the context + in which the program was constructed. + """ + + glUseProgram(self._glsl_program) + + def delete_resource(self): + """ + Delete shaders associated to program. + Note that this happens implicitly when the GL context (widget) is + destroyed, but it won't happen when destroying the python object. + """ + + for uniform_block in self._name_to_uniform_block.values(): + uniform_block.delete_resource() + + glDeleteShader(self._vertex_shader) + glDeleteShader(self._fragment_shader) + glDeleteProgram(self._glsl_program) + + self._vertex_shader = 0 + self._fragment_shader = 0 + self._glsl_program = 0 + self._name_to_uniform_block = {} diff --git a/opengl/modern/image_based_texture.pyx b/opengl/modern/image_based_texture.pyx new file mode 100644 index 00000000..449e0915 --- /dev/null +++ b/opengl/modern/image_based_texture.pyx @@ -0,0 +1,8 @@ +class ImageBasedTexture(DataBasedTexture): + def __init__(self, texture_file): + w, h, rows, info = png.Reader(texture_file).asRGBA8() + data = bytearray(4 * w * h) + for i, row in enumerate(rows): + data[i * 4 * w : (i + 1) * 4 * w] = row + + super().__init__(w, h, data) diff --git a/opengl/modern/simple_image_shader_widget.pyx b/opengl/modern/simple_image_shader_widget.pyx new file mode 100644 index 00000000..72404869 --- /dev/null +++ b/opengl/modern/simple_image_shader_widget.pyx @@ -0,0 +1,269 @@ +class SimpleImageShaderWidget(RawOpenGLWidget): + """ + An image shader is a GLSL program that does all of its computation in + the fragment shader. + + This widget displays an image generated by an image + shader. The draw method simply draws a triangle covering the + entire screen, which causes the fragment shader to be run on + every pixel in the window. The fragment shader source and - + optionally - sizes and offsets for uniform buffer blocks can + be set with set_fragment_shader_source. + + The uniform vec2 viewportSize contains the view port size. + """ + + profile = '3_2' + + vertex_shader_source = b""" + #version 150 + + // Note that GLSL ES 3.0 is based on GLSL 3.30 and used for WebGL 2.0. + // GLSL 1.50 came with OpenGL 3.2. + + in vec4 position; + + void main() + { + gl_Position = position; // no-op + } + + """ + + fragment_shader_source = b""" + #version 150 + + out vec4 out_FragColor; + + void main() + { + out_FragColor = vec4(1.0, 0.0, 0.0, 1.0); + } + """ + + def __init__(self, master, + **kw): + RawOpenGLWidget.__init__(self, master, **kw) + + self._vertex_buffer = VertexBuffer() + self._vertex_buffer.load(((3,-1), (-1,3), (-1,-1))) + + self.image_shader = GLSLProgram( + self.vertex_shader_source, + self.fragment_shader_source, + name = "fallback image shader") + self.textures = [] + self.report_time_callback = None + + def set_textures(self, texture_files): + self.make_current() + + for texture in self.textures: + texture.delete_resource() + + self.textures = [] + for texture_file in texture_files: + texture = None + try: + texture = ImageBasedTexture(texture_file) + except Exception as e: + print("Warning could not read texture %s" % texture_file) + print(e) + + self.textures.append(texture) + + def set_fragment_shader_source(self, + source, + uniform_block_names_sizes_and_offsets = []): + self.image_shader.delete_resource() + self.image_shader = GLSLProgram( + self.vertex_shader_source, + source, + uniform_block_names_sizes_and_offsets = uniform_block_names_sizes_and_offsets, + name = "image shader") + + def render_to_array(self, width, height, as_float = False): + """ + Renders the image into an off-screen framebuffer + of given width and height and returns the result as an array. + + The array either holds unsigned byte or float RGB. + """ + + cdef GLuint fbo + cdef GLenum color_texture_type + cdef GLuint color_texture + cdef GLuint depth_texture + cdef array_type + cdef array.array c_array + + self.make_current() + + if as_float: + array_type = 'f' + color_texture_type = GL_FLOAT + else: + array_type = 'B' + color_texture_type = GL_UNSIGNED_BYTE + + # Create texture for color attachment + glGenTextures(1, &color_texture) + glBindTexture(GL_TEXTURE_2D, color_texture) + glTexImage2D(GL_TEXTURE_2D, 0, + GL_RGB, width, height, + 0, + GL_RGB, + color_texture_type, + NULL) + glTexParameteri(GL_TEXTURE_2D, + GL_TEXTURE_MIN_FILTER, + GL_LINEAR) + glTexParameteri(GL_TEXTURE_2D, + GL_TEXTURE_MAG_FILTER, + GL_LINEAR) + + # Create texture for depth attachment + glGenTextures(1, &depth_texture) + glBindTexture(GL_TEXTURE_2D, depth_texture) + glTexImage2D(GL_TEXTURE_2D, 0, + GL_DEPTH_COMPONENT, width, height, + 0, + GL_DEPTH_COMPONENT, + GL_FLOAT, + NULL) + glTexParameteri(GL_TEXTURE_2D, + GL_TEXTURE_MIN_FILTER, + GL_LINEAR) + glTexParameteri(GL_TEXTURE_2D, + GL_TEXTURE_MAG_FILTER, + GL_LINEAR) + + # Create framebuffer + glGenFramebuffers(1, &fbo) + glBindFramebuffer(GL_FRAMEBUFFER, fbo) + glFramebufferTexture2D(GL_FRAMEBUFFER, + GL_COLOR_ATTACHMENT0, + GL_TEXTURE_2D, + color_texture, 0) + glFramebufferTexture2D(GL_FRAMEBUFFER, + GL_DEPTH_ATTACHMENT, + GL_TEXTURE_2D, + depth_texture, 0) + if glCheckFramebufferStatus(GL_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE: + raise Exception("Incomplete framebuffer") + + # Render into the framebuffer + self.redraw(width, height, + skip_swap_buffers = True) + glFinish() + + # Allocate memory and read framebuffer into it + c_array = array.array(array_type) + array.resize(c_array, 3 * width * height) + glReadPixels(0, 0, width, height, + GL_RGB, + color_texture_type, + c_array.data.as_voidptr) + + # Unbind framebuffer so that stuff is rendered + # to screen again. + glBindFramebuffer(GL_FRAMEBUFFER, 0) + + glDeleteFramebuffers(1, &fbo) + glDeleteTextures(1, &color_texture) + glDeleteTextures(1, &depth_texture) + + print_gl_errors("Render to off-screen area") + + return c_array + + def save_image(self, width, height, outfile): + """ + Writes image of given width and height + as png to given outfile (file object returned by, + .e.g, open("myFile.png", "wb")). + """ + + data = self.render_to_array(width, height) + + # Png writer expects row - we also need to + # flip the image vertically. + stride = 3 * width + rows = [ data[i * stride : (i+1) * stride] + for i in range(height - 1, -1, -1) ] + + writer = png.Writer( + width, height, + greyscale = False, + bitdepth = 8, + alpha = False) + writer.write(outfile, rows) + + def read_depth_value(self, x, y): + + cdef GLfloat depth + + width = self.winfo_width() + height = self.winfo_height() + + self.make_current() + + self.redraw(width, height, + skip_swap_buffers = True, + include_depth_value = True) + glFinish() + + glReadPixels(x, height - y, 1, 1, + GL_DEPTH_COMPONENT, + GL_FLOAT, &depth) + + return (depth, width, height) + + def redraw(self, width, height, + skip_swap_buffers = False, + include_depth_value = False): + + if self.report_time_callback: + start_time = time.time() + + glViewport(0, 0, width, height) + if include_depth_value: + # Writes to z-buffer are only done when GL_DEPTH_TEST + # is enabled + glClear(GL_DEPTH_BUFFER_BIT) + glEnable(GL_DEPTH_TEST) + else: + glDisable(GL_DEPTH_TEST) + glDisable(GL_BLEND) + glDisable(GL_CULL_FACE) + + if self.image_shader.is_valid(): + for i, texture in enumerate(self.textures): + if texture: + glActiveTexture(GL_TEXTURE0 + i) + texture.bind() + + self.image_shader.use_program() + self.image_shader.bind_uniforms( + self.get_uniform_bindings(width, height)) + self._vertex_buffer.bind() + + glDrawArrays(GL_TRIANGLES, 0, 3) + + for i, texture in enumerate(self.textures): + if texture: + glActiveTexture(GL_TEXTURE0 + i) + texture.unbind() + glActiveTexture(GL_TEXTURE0) + + if self.report_time_callback: + glFinish() + self.report_time_callback(time.time() - start_time) + + if not skip_swap_buffers: + self.swap_buffers() + + def get_uniform_bindings(self, view_width, view_height): + return { + 'viewportSize' : ('vec2', (view_width, view_height)) + } diff --git a/opengl/modern/uniform_buffer_object.pyx b/opengl/modern/uniform_buffer_object.pyx new file mode 100644 index 00000000..aed31bd4 --- /dev/null +++ b/opengl/modern/uniform_buffer_object.pyx @@ -0,0 +1,119 @@ +cdef GLfloat* _convert_matrices_to_floats( + matrices, num_matrices, num_rows, num_columns): + cdef GLfloat * floats + floats = malloc( + num_matrices * num_rows * num_columns * sizeof(GLfloat)) + for i in range(num_matrices): + for j in range(num_rows): + for k in range(num_columns): + floats[num_rows * num_columns * i + num_columns * j + k] = ( + matrices[i][j][k]) + return floats + +cdef class UniformBufferObject: + cdef size_t _buffer_size + cdef char * _buffer + cdef GLuint _uniform_buffer_object + cdef _name_to_offset + + def __init__(self, buffer_size, name_to_offset): + self._buffer = NULL + self._buffer_size = 0 + self._uniform_buffer_object = 0 + + self._name_to_offset = name_to_offset + self._buffer_size = buffer_size + self._buffer = (malloc(self._buffer_size)) + + glGenBuffers(1, &self._uniform_buffer_object) + glBindBuffer(GL_UNIFORM_BUFFER, self._uniform_buffer_object) + glBufferData(GL_UNIFORM_BUFFER, + self._buffer_size, + NULL, + GL_STATIC_DRAW) + glBindBuffer(GL_UNIFORM_BUFFER, 0) + + print_gl_errors("UniformBufferObject.__init__") + + def set(self, name, uniform_type, value): + cdef size_t offset + cdef size_t l + cdef size_t i + cdef size_t j + cdef size_t k + cdef GLfloat * float_array + + offset = self._name_to_offset[name] + + if uniform_type == 'vec4[]': + l = len(value) + if offset + 4 * 4 * l > self._buffer_size: + raise Exception( + ("Data for %s of length %d at offset %d not fitting " + "into uniform buffer object of size %d") % ( + name, l, offset, self._buffer_size)) + + float_array = (self._buffer + offset) + for i in range(l): + for j in range(4): + float_array[4 * i + j] = value[i][j] + elif uniform_type == 'mat4[]': + l = len(value) + if offset + 4 * 4 * 4 * l > self._buffer_size: + raise Exception( + ("Data for %s of length %d at offset %d not fitting " + "into uniform buffer object of size %d") % ( + name, l, offset, self._buffer_size)) + + float_array = (self._buffer + offset) + for i in range(l): + for j in range(4): + for k in range(4): + float_array[4 * 4 * i + 4 * j + k] = value[i][j][k] + elif uniform_type == 'int[]': + l = len(value) + if offset + 16 * l > self._buffer_size: + raise Exception( + ("Data for %s of length %d at offset %d not fitting " + "into uniform buffer object of size %d") % ( + name, l, offset, self._buffer_size)) + + int_array = (self._buffer + offset) + for i in range(l): + int_array[4 * i] = value[i] + elif uniform_type == 'float[]': + l = len(value) + if offset + 16 * l > self._buffer_size: + raise Exception( + ("Data for %s of length %d at offset %d not fitting " + "into uniform buffer object of size %d") % ( + name, l, offset, self._buffer_size)) + + float_array = (self._buffer + offset) + for i in range(l): + float_array[4 * i] = value[i] + else: + raise Exception( + ("Unsupported uniform type %s for " + "uniform %s in uniform block") % ( + uniform_type, name)) + + def commit(self): + glBindBuffer(GL_UNIFORM_BUFFER, self._uniform_buffer_object) + glBufferData(GL_UNIFORM_BUFFER, + self._buffer_size, + self._buffer, + GL_STATIC_DRAW) + + def bind_block(self, GLuint binding_point): + glBindBufferBase(GL_UNIFORM_BUFFER, + binding_point, + self._uniform_buffer_object) + + def __dealloc__(self): + free(self._buffer) + self._buffer = NULL + + def delete_resource(self): + glDeleteBuffers(1, &self._uniform_buffer_object) + self._uniform_buffer_object = 0 diff --git a/opengl/modern/vertex_buffer.pyx b/opengl/modern/vertex_buffer.pyx new file mode 100644 index 00000000..4a91d291 --- /dev/null +++ b/opengl/modern/vertex_buffer.pyx @@ -0,0 +1,115 @@ +cdef class VertexBuffer: + """ + Encapsulates a vertex array and vertex buffer object. + + Data can be put into the vertex buffer object with load. + To bind the GL objects so that a shader can consume them (as vertex + attribute 0), call bind. + + For now, it only supports a single vertex buffer object holding + float 1-, 2-, 3-, or 4-vectors. In the future, we might + support having several vertex buffer objects. + """ + + # vertex array object + cdef GLuint _vao + + # Note: _vbo and _dimension would need to be an array + # if we were to support several vertex buffer objects + # Note: we would need to add GLEnum _type to remember the + # type if we support vertex buffer objects different from + # float. + + # vertex buffer + cdef GLuint _vbo + # Whether we loaded 1-, 2-, 3-, 4-vectors into buffer + cdef unsigned int _dimension + + def __cinit__(self): + # Set the indices to zero so that it is safe to call bind and + # delete on them when glGen... fails. + self._vao = 0 + self._vbo = 0 + + self._dimension = 4 + + clear_gl_errors() + + # Note that vertex array objects have some issues + # on Mac OS X. Checking for errors here. + + glGenVertexArrays(1, &self._vao) + print_gl_errors("glGenVertexArrays") + + glBindVertexArray(self._vao) + print_gl_errors("glBindVertexArray") + + glGenBuffers(1, &self._vbo) + print_gl_errors("glGenBuffers") + + def bind(self): + clear_gl_errors() + glBindVertexArray(self._vao) + print_gl_errors("glBindVertexArray") + + glBindBuffer(GL_ARRAY_BUFFER, self._vbo) + + # Use that vertex buffer as vertex attribute 0 + # (i.e., the shader's first "in vec4" will be fed by the + # buffer). + glEnableVertexAttribArray(0) + print_gl_errors("glEnableVertexAttribArray") + + # Specify that the buffer is interpreted as pairs of floats (x,y) + # GLSL will complete this to (x,y,0,1) + glVertexAttribPointer(0, self._dimension, + GL_FLOAT, GL_FALSE, + sizeof(GLfloat) * self._dimension, + NULL) + print_gl_errors("glVertexAttribPointer") + + def load(self, vertex_data): + """ + Load data into GL vertex buffer objects. + vertex_data needs to be something like + [[0,0,0],[1,1,0],[0,1,1],[1,0,1]]. + """ + + cdef unsigned int i + cdef unsigned int j + + self._dimension = len(vertex_data[0]) + + cdef size_t num_bytes = ( + sizeof(GLfloat) * self._dimension * len(vertex_data)) + + cdef GLfloat * verts = malloc(num_bytes) + try: + for i, vertex in enumerate(vertex_data): + for j in range(self._dimension): + verts[self._dimension * i + j] = vertex[j] + + clear_gl_errors() + # This is not really necessary to push data to + # the GL_ARRAY_BUFFER. + glBindVertexArray(self._vao) + print_gl_errors("glBindVertexArray") + + glBindBuffer(GL_ARRAY_BUFFER, self._vbo) + + glBufferData(GL_ARRAY_BUFFER, + num_bytes, + verts, + GL_STATIC_DRAW) + + finally: + free(verts) + + def delete_resource(self): + # Same comments as for GLSLProgram.delete_resource apply + + glDeleteBuffers(1, &self._vbo) + glDeleteVertexArrays(1, &self._vao) + + self._vbo = 0 + self._vao = 0