Skip to content

Instantly share code, notes, and snippets.

@leon-nn
Created January 26, 2018 20:30
Show Gist options
  • Save leon-nn/cd4e3d50eb0fa23d8e197102f49f2cb3 to your computer and use it in GitHub Desktop.
Save leon-nn/cd4e3d50eb0fa23d8e197102f49f2cb3 to your computer and use it in GitHub Desktop.
This is a trivial example to demonstrate how to use modern OpenGL in Python via PyOpenGL to render offscreen to a texture buffer in a framebuffer object
from OpenGL.GL import *
from OpenGL.GLUT import *
import numpy as np
import matplotlib.pyplot as plt
# Global variables
shaderProgram = None
vertexBufferObject = None
indexBufferObject = None
framebufferObject = None
vertexArrayObject = None
# Strings containing shader programs written in GLSL
vertexShaderString = """
#version 330
layout(location = 0) in vec3 windowCoordinates;
layout(location = 1) in vec3 vertexColor;
smooth out vec3 fragmentColor;
uniform mat4 windowToClipMat;
void main()
{
gl_Position = windowToClipMat * vec4(windowCoordinates, 1.0f);
fragmentColor = vertexColor;
}
"""
fragmentShaderString = """
#version 330
smooth in vec3 fragmentColor;
out vec4 pixelColor;
void main()
{
pixelColor = vec4(fragmentColor, 1.);
}
"""
def initializeShaders(shaderDict):
"""
Compiles each shader defined in shaderDict, attaches them to a program object, and links them (i.e., creates executables that will be run on the vertex, geometry, and fragment processors on the GPU). This is more-or-less boilerplate.
"""
shaderObjects = []
global shaderProgram
shaderProgram = glCreateProgram()
for shaderType, shaderString in shaderDict.items():
shaderObjects.append(glCreateShader(shaderType))
glShaderSource(shaderObjects[-1], shaderString)
glCompileShader(shaderObjects[-1])
status = glGetShaderiv(shaderObjects[-1], GL_COMPILE_STATUS)
if status == GL_FALSE:
if shaderType is GL_VERTEX_SHADER:
strShaderType = "vertex"
elif shaderType is GL_GEOMETRY_SHADER:
strShaderType = "geometry"
elif shaderType is GL_FRAGMENT_SHADER:
strShaderType = "fragment"
raise RuntimeError("Compilation failure (" + strShaderType + " shader):\n" + glGetShaderInfoLog(shaderObjects[-1]).decode('utf-8'))
glAttachShader(shaderProgram, shaderObjects[-1])
glLinkProgram(shaderProgram)
status = glGetProgramiv(shaderProgram, GL_LINK_STATUS)
if status == GL_FALSE:
raise RuntimeError("Link failure:\n" + glGetProgramInfoLog(shaderProgram).decode('utf-8'))
for shader in shaderObjects:
glDetachShader(shaderProgram, shader)
glDeleteShader(shader)
def windowToClip(width, height, zNear, zFar):
"""
Creates elements for an OpenGL-style column-based homogenous transformation matrix that maps points from window space to clip space.
"""
windowToClipMat = np.zeros(16, dtype = np.float32)
windowToClipMat[0] = 2 / width
windowToClipMat[3] = -1
windowToClipMat[5] = 2 / height
windowToClipMat[7] = -1
windowToClipMat[10] = 2 / (zFar - zNear)
windowToClipMat[11] = -(zFar + zNear) / (zFar - zNear)
windowToClipMat[15] = 1
return windowToClipMat
def configureShaders(var):
"""
Modifies the window-to-clip space transform matrix in the vertex shader, but you can use this to configure your shaders however you'd like, of course.
"""
# Grabs the handle for the uniform input from the shader
windowToClipMatUnif = glGetUniformLocation(shaderProgram, "windowToClipMat")
# Get input parameters to define a matrix that will be used as the uniform input
width, height, zNear, zFar = var
windowToClipMat = windowToClip(width, height, zNear, zFar)
# Assign the matrix to the uniform input in our shader program. Note that GL_TRUE here transposes the matrix because of OpenGL conventions
glUseProgram(shaderProgram)
glUniformMatrix4fv(windowToClipMatUnif, 1, GL_TRUE, windowToClipMat)
# We're finished modifying our shader, so we set the program to be used as null to be proper
glUseProgram(0)
def initializeVertexBuffer():
"""
Assign the triangular mesh data and the triplets of vertex indices that form the triangles (index data) to VBOs
"""
# Create a handle and assign the VBO for the mesh data to it
global vertexBufferObject
vertexBufferObject = glGenBuffers(1)
# Bind the VBO to the GL_ARRAY_BUFFER target in the OpenGL context
glBindBuffer(GL_ARRAY_BUFFER, vertexBufferObject)
# Allocate enough memory for this VBO to contain the mesh data
glBufferData(GL_ARRAY_BUFFER, meshData, GL_STATIC_DRAW)
# Unbind the VBO from the target to be proper
glBindBuffer(GL_ARRAY_BUFFER, 0)
# Similar to the above, but for the index data
global indexBufferObject
indexBufferObject = glGenBuffers(1)
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, indexBufferObject)
glBufferData(GL_ELEMENT_ARRAY_BUFFER, indexData, GL_STATIC_DRAW)
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0)
def initializeFramebufferObject():
"""
Create an FBO and assign a texture buffer to it for the purpose of offscreen rendering to the texture buffer
"""
# Create a handle and assign a texture buffer to it
renderedTexture = glGenTextures(1)
# Bind the texture buffer to the GL_TEXTURE_2D target in the OpenGL context
glBindTexture(GL_TEXTURE_2D, renderedTexture)
# Attach a texture 'img' (which should be of unsigned bytes) to the texture buffer. If you don't want a specific texture, you can just replace 'img' with 'None'.
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, width, height, 0, GL_RGB, GL_UNSIGNED_BYTE, img)
# Does some filtering on the texture in the texture buffer
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST)
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST)
# Unbind the texture buffer from the GL_TEXTURE_2D target in the OpenGL context
glBindTexture(GL_TEXTURE_2D, 0)
# Create a handle and assign a renderbuffer to it
depthRenderbuffer = glGenRenderbuffers(1)
# Bind the renderbuffer to the GL_RENDERBUFFER target in the OpenGL context
glBindRenderbuffer(GL_RENDERBUFFER, depthRenderbuffer)
# Allocate enough memory for the renderbuffer to hold depth values for the texture
glRenderbufferStorage(GL_RENDERBUFFER, GL_DEPTH_COMPONENT, width, height)
# Unbind the renderbuffer from the GL_RENDERBUFFER target in the OpenGL context
glBindRenderbuffer(GL_RENDERBUFFER, 0)
# Create a handle and assign the FBO to it
global framebufferObject
framebufferObject = glGenFramebuffers(1)
# Use our initialized FBO instead of the default GLUT framebuffer
glBindFramebuffer(GL_FRAMEBUFFER, framebufferObject)
# Attaches the texture buffer created above to the GL_COLOR_ATTACHMENT0 attachment point of the FBO
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, renderedTexture, 0)
# Attaches the renderbuffer created above to the GL_DEPTH_ATTACHMENT attachment point of the FBO
glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, GL_RENDERBUFFER, depthRenderbuffer)
# Sees if your GPU can handle the FBO configuration defined above
if glCheckFramebufferStatus(GL_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE:
raise RuntimeError('Framebuffer binding failed, probably because your GPU does not support this FBO configuration.')
# Unbind the FBO, relinquishing the GL_FRAMEBUFFER back to the window manager (i.e. GLUT)
glBindFramebuffer(GL_FRAMEBUFFER, 0)
def resetFramebufferObject():
"""
Clears the color and depth in the FBO
"""
# Use our initialized FBO instead of the default GLUT framebuffer
glBindFramebuffer(GL_FRAMEBUFFER, framebufferObject)
# Clears any color or depth information in the FBO
glClearColor(0.0, 0.0, 0.0, 1.0)
glClearDepth(1.0)
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT)
# Unbind the FBO, relinquishing the GL_FRAMEBUFFER back to the window manager (i.e. GLUT)
glBindFramebuffer(GL_FRAMEBUFFER, 0)
def initializeVertexArray():
"""
Creates the VAO to store the VBOs for the mesh data and the index data,
"""
# Create a handle and assign a VAO to it
global vertexArrayObject
vertexArrayObject = glGenVertexArrays(1)
# Set the VAO as the currently used object in the OpenGL context
glBindVertexArray(vertexArrayObject)
# Bind the VBO for the mesh data to the GL_ARRAY_BUFFER target in the OpenGL context
glBindBuffer(GL_ARRAY_BUFFER, vertexBufferObject)
# Specify the location indices for the types of inputs to the shaders
glEnableVertexAttribArray(0)
glEnableVertexAttribArray(1)
# Assign the first type of input (which is stored in the VBO beginning at the offset in the fifth argument) to the shaders
glVertexAttribPointer(0, vertexDim, GL_FLOAT, GL_FALSE, 0, None)
# Calculate the offset in the VBO for the second type of input, specified in bytes (because we used np.float32, each element is 4 bytes)
colorOffset = c_void_p(vertexDim * numVertices * 4)
# Assign the second type of input, beginning at the offset calculated above, to the shaders
glVertexAttribPointer(1, vertexDim, GL_FLOAT, GL_FALSE, 0, colorOffset)
# Bind the VBO for the index data to the GL_ELEMENT_ARRAY_BUFFER target in the OpenGL context
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, indexBufferObject)
# Unset the VAO as the current object in the OpenGL context
glBindVertexArray(0)
def render():
# Defines what shaders to use
glUseProgram(shaderProgram)
# Use our initialized FBO instead of the default GLUT framebuffer
glBindFramebuffer(GL_FRAMEBUFFER, framebufferObject)
# Set our initialized VAO as the currently used object in the OpenGL context
glBindVertexArray(vertexArrayObject)
# Draws the mesh triangles defined in the VBO of the VAO above according to the vertices defining the triangles in indexData, which is of unsigned shorts
glDrawElements(GL_TRIANGLES, indexData.size, GL_UNSIGNED_SHORT, None)
# Being proper
glBindVertexArray(0)
glUseProgram(0)
if __name__ == '__main__':
# Predetermine the dimensions of the viewport/window for the rendering
width = 100
height = 100
# Create a test 100x100 RGB image: all black and a red border. This is to verify if our viewport is correct.
img = np.zeros((height, width, 3), dtype = np.uint8)
img[[0, -1], :, 0] = 255
img[:, [0, -1], 0] = 255
# Convert to bytes to store in the texture buffer
img = img.tobytes()
# Define a triangle in window space
x = np.array([50, 40, 60]) - 1
y = np.array([20, 40, 40]) - 1
z = np.ones(x.size)
vertexCoords = np.c_[x, y, z].astype(np.float32)
# Specify the vertex indices for the triangle and convert to unsigned short to store in a VBO (because that's how we defined the VBO for the index data)
indexData = np.array([[0, 1, 2]], dtype = np.uint16)
# Define colors to each vertex in the triangle
vertexColors = np.eye(3, dtype = np.float32)
# Stack the vertex coordinates on top of the vertex colors to store in a VBO. Note that this should be of float32 because that's how we defined the VBO for the mesh data.
meshData = np.r_[vertexCoords, vertexColors]
# Some useful parameters to calculate pointers for the mesh data VBO
vertexDim = 3
numVertices = vertexCoords.shape[0]
# Some useful parameters to calculate the window space to clip space matrix
zNear = -10
zFar = 10
# You can use any means to initialize an OpenGL context (e.g. GLUT), but since we're rendering offscreen to an FBO, we don't need to bother to display, which is why we hide the GLUT window.
glutInit()
window = glutCreateWindow('Merely creating an OpenGL context...')
glutHideWindow()
# Organize the strings defining our shaders into a dictionary
shaderDict = {GL_VERTEX_SHADER: vertexShaderString, GL_FRAGMENT_SHADER: fragmentShaderString}
# Use this dictionary to compile the shaders and link them to the GPU processors
initializeShaders(shaderDict)
# A utility function to modify uniform inputs to the shaders
configureShaders([width, height, zNear, zFar])
# Set the dimensions of the viewport
glViewport(0, 0, width, height)
# Performs face culling
glEnable(GL_CULL_FACE)
glCullFace(GL_BACK)
glFrontFace(GL_CW)
# Performs z-buffer testing
glEnable(GL_DEPTH_TEST)
glDepthMask(GL_TRUE)
glDepthFunc(GL_LEQUAL)
glDepthRange(0.0, 1.0)
# With all of our data defined, we can initialize our VBOs, FBO, and VAO to the OpenGL context to prepare for rendering
initializeVertexBuffer()
initializeFramebufferObject()
initializeVertexArray()
# Then we render offscreen to the FBO
render()
# Configure how we store the pixels in memory for our subsequent reading of the FBO to store the rendering into memory. The second argument specifies that our pixels will be in bytes.
glPixelStorei(GL_PACK_ALIGNMENT, 1)
# Set the color buffer that we want to read from. The GL_COLORATTACHMENT0 target is where we assigned our texture buffer in the FBO.
glReadBuffer(GL_COLOR_ATTACHMENT0)
# Now we do the actual reading, noting that we read the pixels in the buffer as unsigned bytes to be consistent will how they are stored
data = glReadPixels(0, 0, width, height, GL_RGB, GL_UNSIGNED_BYTE)
# Convert the unsigned bytes to a NumPy array for whatever needs you may have
rendering = np.frombuffer(data, dtype = np.uint8).reshape(height, width, 3)
plt.imshow(rendering)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment