Search code examples
pythonopenglwxpythontexturespyopengl

Displaying a simple 2D textured rectangle inside a wxGLCanvas using PyOpenGL


I'm trying to write some code that will display a single image as a simple 2D textured rectangle within a wxGLCanvas container.

Here's a runnable example of what I've got so far:

import wx
from wxPython.glcanvas import wxGLCanvas
from OpenGL.GLU import *
from OpenGL.GL import *
import numpy as np
from scipy.misc import ascent


def run():
    imarray = np.float32(ascent())
    imarray = np.repeat(imarray[..., np.newaxis], 3, axis=2)
    app = wx.PySimpleApp()
    frame = wx.Frame(None, title='Simple 2D texture')
    canvas = myGLCanvas(frame, imarray)
    frame.Show(True)
    app.MainLoop()


class myGLCanvas(wxGLCanvas):

    def __init__(self, parent, image):
        attribList = [wx.glcanvas.WX_GL_DOUBLEBUFFER]
        wxGLCanvas.__init__(self, parent, -1, attribList=attribList)
        wx.EVT_PAINT(self, self.OnPaint)
        self.image = np.float32(image)
        self.InitProjection()
        self.InitTexture()
        pass

    def OnPaint(self, event):
        """Called whenever the window gets resized, uncovered etc."""
        self.SetCurrent()
        dc = wx.PaintDC(self)
        self.OnDraw()
        self.SwapBuffers()
        pass

    def InitProjection(self):
        """Enable the depth buffer and initialize the viewport and
        projection matrix"""
        glEnable(GL_DEPTH_TEST)
        glDepthFunc(GL_LEQUAL)
        width = self.image.shape[1]
        height = self.image.shape[0]
        glViewport(0, 0, width, height)
        glMatrixMode(GL_PROJECTION)
        glLoadIdentity()
        glOrtho(0, width, height, 0, -1, 1)
        pass

    def InitTexture(self):
        """Initializes the texture from a numpy array"""
        self.texture = glGenTextures(1)
        glEnable(GL_TEXTURE_2D)
        glPixelStorei(GL_UNPACK_ALIGNMENT, 1)
        glBindTexture(GL_TEXTURE_2D, self.texture)
        glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR)
        glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR)
        glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP)
        glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP)
        glTexEnvf(GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_MODULATE)
        glTexImage2D(GL_TEXTURE_2D, 0,
                     GL_RGB,
                     self.image.shape[1], self.image.shape[0], 0,
                     GL_RGB,
                     GL_FLOAT,
                     self.image)
        glDisable(GL_TEXTURE_2D)
        pass

    def OnDraw(self):
        """Draw a textured rectangle slightly smaller than the viewport"""
        glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT)
        glClearColor(0., 0., 0., 0.)
        glClearDepth(1)
        glMatrixMode(GL_MODELVIEW)
        glLoadIdentity()
        glBindTexture(GL_TEXTURE_2D, self.texture)
        glEnable(GL_TEXTURE_2D)
        # draw a textured quad, shrink it a bit so the edge is clear
        glBegin(GL_QUADS)
        glTexCoord2f(0., 0.)
        glVertex3f(-0.9, -0.9, 0.)
        glTexCoord2f(1., 0.)
        glVertex3f(0.9, -0.9, 0.)
        glTexCoord2f(1., 1.)
        glVertex3f(0.9, 0.9, 0.)
        glTexCoord2f(0., 1.)
        glVertex3f(-0.9, 0.9, 0.)
        glEnd()
        glDisable(GL_TEXTURE_2D)
        pass

if __name__ == '__main__':
    run()

This draws a rectangle successfully, but fails to texture it - all I see is a white rectangle. Any idea what I'm doing wrong?


Solution

  • Well, the critical mistake I was making was not scaling the float values of my input data to between 0 and 1 - all my pixels were >1 so the quad rendered as white! I had a bit of trouble finding good examples of how to use wxGLCanvases, so here's a simple texture rendered in a wxGLCanvas for anyone who might find it useful.

    import numpy as np
    from scipy.misc import ascent
    import OpenGL.GL as gl
    import wx
    from wx.glcanvas import GLCanvas
    
    
    class Canvas(GLCanvas):
    
        def __init__(self, parent):
            """create the canvas """
            super(Canvas, self).__init__(parent)
            self.texture = None
    
            # execute self.onPaint whenever the parent frame is repainted
            wx.EVT_PAINT(self, self.onPaint)
    
        def initTexture(self):
            """init the texture - this has to happen after an OpenGL context
            has been created
            """
    
            # make the OpenGL context associated with this canvas the current one
            self.SetCurrent()
    
            data = np.uint8(np.flipud(ascent()))
            w, h = data.shape
    
            # generate a texture id, make it current
            self.texture = gl.glGenTextures(1)
            gl.glBindTexture(gl.GL_TEXTURE_2D, self.texture)
    
            # texture mode and parameters controlling wrapping and scaling
            gl.glTexEnvf(gl.GL_TEXTURE_ENV, gl.GL_TEXTURE_ENV_MODE, gl.GL_MODULATE)
            gl.glTexParameterf(
                gl.GL_TEXTURE_2D, gl.GL_TEXTURE_WRAP_S, gl.GL_REPEAT)
            gl.glTexParameterf(
                gl.GL_TEXTURE_2D, gl.GL_TEXTURE_WRAP_T, gl.GL_REPEAT)
            gl.glTexParameterf(
                gl.GL_TEXTURE_2D, gl.GL_TEXTURE_MAG_FILTER, gl.GL_LINEAR)
            gl.glTexParameterf(
                gl.GL_TEXTURE_2D, gl.GL_TEXTURE_MIN_FILTER, gl.GL_LINEAR)
    
            # map the image data to the texture. note that if the input
            # type is GL_FLOAT, the values must be in the range [0..1]
            gl.glTexImage2D(gl.GL_TEXTURE_2D, 0, gl.GL_RGB, w, h, 0,
                            gl.GL_LUMINANCE, gl.GL_UNSIGNED_BYTE, data)
    
        def onPaint(self, event):
            """called when window is repainted """
            # make sure we have a texture to draw
            if not self.texture:
                self.initTexture()
            self.onDraw()
    
        def onDraw(self):
            """draw function """
    
            # make the OpenGL context associated with this canvas the current one
            self.SetCurrent()
    
            # set the viewport and projection
            w, h = self.GetSize()
            gl.glViewport(0, 0, w, h)
    
            gl.glMatrixMode(gl.GL_PROJECTION)
            gl.glLoadIdentity()
            gl.glOrtho(0, 1, 0, 1, 0, 1)
    
            gl.glMatrixMode(gl.GL_MODELVIEW)
            gl.glLoadIdentity()
            gl.glClear(gl.GL_COLOR_BUFFER_BIT)
    
            # enable textures, bind to our texture
            gl.glEnable(gl.GL_TEXTURE_2D)
            gl.glBindTexture(gl.GL_TEXTURE_2D, self.texture)
            gl.glColor3f(1, 1, 1)
    
            # draw a quad
            gl.glBegin(gl.GL_QUADS)
            gl.glTexCoord2f(0, 1)
            gl.glVertex2f(0, 1)
            gl.glTexCoord2f(0, 0)
            gl.glVertex2f(0, 0)
            gl.glTexCoord2f(1, 0)
            gl.glVertex2f(1, 0)
            gl.glTexCoord2f(1, 1)
            gl.glVertex2f(1, 1)
            gl.glEnd()
    
            gl.glDisable(gl.GL_TEXTURE_2D)
    
            # swap the front and back buffers so that the texture is visible
            self.SwapBuffers()
    
    
    def run():
        app = wx.App()
        fr = wx.Frame(None, size=(512, 512), title='wxPython texture demo')
        canv = Canvas(fr)
        fr.Show()
        app.MainLoop()
    
    if __name__ == "__main__":
        run()