rss_feed

Python pixel shading example using point sprites

homeHome
pagespython
pagesopengl
pagessprites

Point sprite shader

OpenGL pixel shader source code

This example shows rendering textures using GLPOINTS primitives, it should work on most modern hardware I have hit issues on older intel cards which seems to be a driver bug causing nothing to display. If you encouter this try setting the shader code to a solid colour to see if the pixels have size.

OpenGL provides a point size parameter which we use to give our points size, there is a similar option for lines. This has the benefit of sending less data to the gpu improving efficiency, the other option is to send 4 vertices instead of one and rendering the texture to two triangles.

We need to enable support for pixel size using glEnable, the appropriate settings are in the helper file and are set when the window is setup. The main one being GLPOINT SPRITE and GLPOINT SPRITEARB also glPointSize() if your using a fixed function pipeline instead of shaders, if your using a shader you can set gl PointSize in your vertex shader.

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
#!/usr/bin/env python
import os
import sys
import time
import random
import pprint

import Xlib
from Xlib.display import Display
from gi.repository import Gtk, Gdk, GdkX11, GLib, GObject
from numpy import array

from OpenGL.GL import *
from OpenGL.GLU import gluPerspective, gluLookAt
from OpenGL.arrays import vbo
from OpenGL import GLX

from OpenGL.GL import GL_VERTEX_SHADER, GL_FRAGMENT_SHADER
from OpenGL.GL import shaders, glGetUniformLocation


from helper import shader, gtkgl


class scene:
    width, height = 600, 400
    camera_distance = 25
    texture_id = None

    def __init__(self):
        """setup everything in the correct order"""
        self.glwrap = gtkgl()
        self.setup_opengl()
        self.generate()
        self.gui()

    def gui(self):
        """load in the gui and connect the events and set our properties"""
        self.start_time = time.time()
        self.frame = 1
        xml = Gtk.Builder()
        xml.add_from_file('gui.glade')

        self.window = xml.get_object('window1')
        self.button = xml.get_object('btngenerate')

        self.canvas_widget = xml.get_object('canvas')
        self.canvas_widget.connect('configure_event', self.on_configure_event)
        self.canvas_widget.connect('draw', self.on_draw)
        self.canvas_widget.set_double_buffered(False)
        self.canvas_widget.set_size_request(self.glwrap.width, self.glwrap.height)

        self.button.connect('pressed', self.generate)

        self.window.show_all()
        self.setup_shaders()
        GObject.idle_add(self.loop_draw)


    def loop_draw(self):
        #send redraw event to drawing area widget
        self.canvas_widget.queue_draw()
        return True

    def on_configure_event(self, widget, event):
        """if we recieve a configure event for example a resize, then grab the context settings and resize our scene """
        self.glwrap.width = widget.get_allocation().width
        self.glwrap.height = widget.get_allocation().height
        self.width, self.height = self.glwrap.width, self.glwrap.height

        #update our states because we have reconfigured the display
        self.glwrap.configure(widget.get_window())
        self.glwrap.draw_start()
        self.update_camera()


        glEnable(GL_TEXTURE_2D)
        glEnable(GL_DEPTH_TEST)
        glDepthMask(GL_TRUE)
        glDepthFunc(GL_LEQUAL)
        glDepthRange(0.0, 1.0)
        glEnable(GL_CULL_FACE)
        glCullFace(GL_BACK)
        glFrontFace(GL_CW)

        self.glwrap.draw_finish()
        return True

    def on_draw(self, widget, context):
        """if we recieve a draw event redraw our opengl scene"""
        self.elapsed_time = time.time() - self.start_time
        self.frame += 1

        if self.elapsed_time > 1:
            print('fps %d' % self.frame)
            self.start_time = time.time()
            self.frame = 1
        self.glwrap.draw_start()
        #self.draw()
        glClearDepth(1.0)
        glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT)
        glLoadIdentity()
        self.draw_shader_points_texture()
        self.glwrap.draw_finish()

    def generate(self, *args):
        """randomly position a few textured points"""
        self.point_sprites = []
        for i in range(0, 10):
            self.point_sprites.append((random.uniform(-8, 8), random.uniform(-8, 8), random.uniform(-8, 8)))

        print('Generated %s points' % str(len(self.point_sprites)))
        self.vertex_vbo = vbo.VBO(array(self.point_sprites, 'f'))

    def setup_shaders(self):
        self.shader_program = shader()
        self.shader_program.compile()
        self.texture_id = self.shader_program.load_image('testing.png')

    def setup_opengl(self):
        glShadeModel(GL_SMOOTH)
        glClearColor(0.0, 0.0, 0.0, 0.0)
        glClearDepth(1.0)
        glHint(GL_PERSPECTIVE_CORRECTION_HINT, GL_NICEST)
        glPolygonMode(GL_FRONT_AND_BACK, GL_FILL)

    def update_camera(self):
        """Setup a very basic camera"""
        glViewport(0, 0, self.width, self.height)
        glMatrixMode(GL_PROJECTION)
        glLoadIdentity()
        gluPerspective(45, 1.0 * self.width / self.height, 1.0, 80.0)
        gluLookAt(self.camera_distance, self.camera_distance, self.camera_distance,     # location
                  0.0, 0.0, 0.0,        # lookat
                  0.0, 1.0, 0.0)        # up direction
        glMatrixMode(GL_MODELVIEW)
        glLoadIdentity()

    def draw_shader_points_texture(self):
        glEnableClientState(GL_VERTEX_ARRAY)
        glEnableClientState(GL_COLOR_ARRAY)
        glEnableClientState(GL_TEXTURE_COORD_ARRAY)
        matrix_model_view = glGetFloatv(GL_MODELVIEW_MATRIX)
        matrix_projection = glGetFloatv(GL_PROJECTION_MATRIX)

        glUseProgram(self.shader_program.program)
        self.vertex_vbo.bind()
        glEnableVertexAttribArray(self.shader_program.point_vertex)
        glVertexAttribPointer(self.shader_program.point_vertex, 3, GL_FLOAT, GL_FALSE, 12, self.vertex_vbo)

        #send the model and projection matrices to the shader 
        glUniformMatrix4fv(self.shader_program.point_matrix_model_view, 1, GL_FALSE, matrix_model_view)
        glUniformMatrix4fv(self.shader_program.point_matrix_projection, 1, GL_FALSE, matrix_projection)

        #make the texture we loaded in on shader initalisation active, passing the texture id supplied a t this 
        glActiveTexture(GL_TEXTURE0)
        glBindTexture(GL_TEXTURE_2D, self.texture_id)
        glUniform1i(self.shader_program.texture_uniform, 0)

        glDrawArrays(GL_POINTS, 0, len(self.vertex_vbo))

        glDisableVertexAttribArray(self.shader_program.point_vertex)
        self.vertex_vbo.unbind()
        glDisableClientState(GL_COLOR_ARRAY)
        glDisableClientState(GL_VERTEX_ARRAY)
        glDisableClientState(GL_TEXTURE_COORD_ARRAY)

    def draw(self):
        glEnable(GL_DEPTH_TEST)
        glClearDepth(1.0)
        glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT)
        glLoadIdentity()
        self.draw_shader_points_texture()


if __name__ == '__main__': 
    glexample = scene()    
    Gtk.main()

This file holds various helper code mainly to load and setup the shaders, it also wraps the compile shader function to fix a bug in the packaged version of pyopengl. This help file also handles loading the texture for the sprites and seting up gtk context for rendering the scene to.

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
import os
import sys
import numpy
from numpy import array
from ctypes import *
from OpenGL.GL import *
from OpenGL import GLX
from OpenGL.GL import shaders
from OpenGL.arrays import vbo
from OpenGL.raw.GL.ARB.point_sprite import GL_POINT_SPRITE_ARB, GL_COORD_REPLACE_ARB
from OpenGL._bytes import bytes, _NULL_8_BYTE

import Xlib
from Xlib.display import Display
try:
    from OpenGL.GLX import struct__XDisplay
except ImportError as err:
    from OpenGL.raw._GLX import struct__XDisplay
from gi.repository import Gtk, Gdk, GdkX11, GLib, GObject

from PIL import Image

def glDebug():
    error = glGetError()
    if error:
        print ('opengl error ' + str(error))
        return True
    return False


class gtkgl:
    """ wrapper to enable opengl in our gtk application
    useful link http://www.opengl.org/wiki/Programming_OpenGL_in_Linux:_GLX_and_Xlib"""
    #these method do not seem to exist in python x11 library lets exploit the c methods 
    xlib = cdll.LoadLibrary('libX11.so')
    xlib.XOpenDisplay.argtypes = [c_char_p]
    xlib.XOpenDisplay.restype = POINTER(struct__XDisplay)
    xdisplay = xlib.XOpenDisplay(None)
    display = Xlib.display.Display()
    attrs = []

    xwindow_id = None
    width, height = 500,300

    def __init__(self):
        """ lets setup are opengl settings and create the context for our window """
        self.add_attribute(GLX.GLX_RGBA, True)
        self.add_attribute(GLX.GLX_RED_SIZE, 8)
        self.add_attribute(GLX.GLX_GREEN_SIZE, 8)
        self.add_attribute(GLX.GLX_BLUE_SIZE, 8)
        self.add_attribute(GLX.GLX_DOUBLEBUFFER, 1)
        self.add_attribute(GLX.GLX_DEPTH_SIZE, 24)

        xvinfo = GLX.glXChooseVisual(self.xdisplay, self.display.get_default_screen(), self.get_attributes())
        print("run glxinfo to match this visual id %s " % hex(xvinfo.contents.visualid))
        self.context = GLX.glXCreateContext(self.xdisplay, xvinfo, None, True)

    def add_attribute(self, setting, value):
        """just to nicely add opengl parameters"""
        self.attrs.append(setting)
        self.attrs.append(value)

    def get_attributes(self):
        """ return our parameters in the expected structure"""
        attrs = self.attrs + [0, 0]
        return (c_int * len(attrs))(*attrs)

    def configure(self, wid):
        """  """
        self.xwindow_id = GdkX11.X11Window.get_xid(wid)
        if(not GLX.glXMakeCurrent(self.xdisplay, self.xwindow_id, self.context)):
            print('failed configuring context')
        glViewport(0, 0, self.width, self.height)

        glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA)
        glEnable(GL_DEPTH_TEST)
        glEnable(GL_BLEND)

        #settings related to enbling pixel sprites.
        glEnable(GL_POINT_SPRITE)
        glEnable(GL_VERTEX_PROGRAM_POINT_SIZE)
        glEnable(GL_PROGRAM_POINT_SIZE)
        glEnable(GL_POINT_SPRITE_ARB)

        #glPointSize(16)

        glShadeModel(GL_SMOOTH)
        glDepthFunc(GL_LEQUAL)
        
        glPolygonMode(GL_FRONT_AND_BACK, GL_FILL)

    def draw_start(self):
        """make cairo context current for drawing"""
        if(not GLX.glXMakeCurrent(self.xdisplay, self.xwindow_id, self.context)):
            print ("failed to get the context for drawing")

    def draw_finish(self):
        """swap buffer when we have finished drawing"""
        GLX.glXSwapBuffers(self.xdisplay, self.xwindow_id)


def compileShader( source, shaderType ):

    """Compile shader source of given type
        source -- GLSL source-code for the shader
    shaderType -- GLenum GL_VERTEX_SHADER, GL_FRAGMENT_SHADER, etc,
        returns GLuint compiled shader reference
    raises RuntimeError when a compilation failure occurs
    """
    if isinstance(source, str):

        source = [source]
    elif isinstance(source, bytes):

        source = [source.decode('utf-8')]

    shader = glCreateShader(shaderType)
    glShaderSource(shader, source)
    glCompileShader(shader)
    result = glGetShaderiv(shader, GL_COMPILE_STATUS)

    if not(result):
        # TODO: this will be wrong if the user has
        # disabled traditional unpacking array support.
        raise RuntimeError(
            """Shader compile failure (%s): %s"""%(
                result,
                glGetShaderInfoLog( shader ),
            ),
            source,
            shaderType,
        )
    return shader


class shader:
    vertex = """#version 120
        //attributes in values
        attribute vec3 vertex_pos;

        uniform mat4 modelview_mat;
        uniform mat4 projection_mat;

        void main(){
            vec4 pos = modelview_mat * vec4(vertex_pos, 1.0);
            gl_Position = projection_mat * pos;
            gl_PointSize = 16.0;
        }"""

    fragment = """#version 120
        uniform sampler2D quad_texture;
        void main(){
            gl_FragColor = texture2D(quad_texture, gl_PointCoord);
            //gl_FragColor = vec4(gl_TexCoord[0].st, 1, 0.75);//for debug
            //gl_FragColor = vec4(gl_PointCoord, 1, 0.75);//for debug
        }"""

    program = None

    def load_image(self, filename):
        """load our image using pil and setup and make it available to opengl"""
        path = os.path.abspath(filename)
        im = Image.open(path)
        try: 
            
            ix, iy, image = im.size[0], im.size[1], im.tostring("raw", "RGBA", 0, -1) 
        except SystemError as error:
            ix, iy, image = im.size[0], im.size[1], im.tostring("raw", "RGBX", 0, -1)
        except:
            return None

        texture_id = glGenTextures(1)
        glDebug()
        glBindTexture(GL_TEXTURE_2D, texture_id)
        glDebug()
        glPixelStorei(GL_UNPACK_ALIGNMENT, 1)
        glDebug()
        glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, ix, iy, 0, GL_RGBA, GL_UNSIGNED_BYTE, image)
        glDebug()

        glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR)
        glDebug()
        glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S,  GL_CLAMP_TO_BORDER)
        glDebug()
        glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T,  GL_CLAMP_TO_BORDER)
        glDebug()

        if glGetError():
            print(glGetError())

        return texture_id

    def compile(self):
        """setup our shaders and compile for rendering our primitives"""
        self.program = shaders.compileProgram(
            compileShader(self.vertex, GL_VERTEX_SHADER),
            compileShader(self.fragment, GL_FRAGMENT_SHADER),)

        self.point_vertex = glGetAttribLocation(self.program, b'vertex_pos')
        self.texture_uniform = glGetUniformLocation(self.program, b'quad_texture')
        self.point_matrix_model_view = glGetUniformLocation(self.program, b"modelview_mat")
        self.point_matrix_projection = glGetUniformLocation(self.program, b"projection_mat")
        self.fixed_quad_tex_coords = [[[0.0, 1.0]], [[1.0, 1.0]], [[0.0, 0.0]], [[1.0, 0.0]]] 
        self.fixed_quad_indices = [0, 1, 2, 1, 2, 3]
        self.fixed_quad_indices_vbo = vbo.VBO(
            array([self.fixed_quad_indices], dtype='uint32'), target=GL_ELEMENT_ARRAY_BUFFER)

class point:
    __slots__ = ['x', 'y', 'z', 'xyz', 'vertex']

    def __init__(self, p, c=(1, 0, 0)):
        """ Position in 3d space as a tuple or list, and colour in tuple or list format"""
        self.x, self.y, self.z = p
        self.vertex = array([self.x, self.y, self.z, c[0], c[1], c[2]], 'f')