The renderer, which has lots of dependencies not included here, follows:
#!/usr/bin/env python
import time
import sys
from pyopengles import *
from geometry import *
from glop import *
WIDTH = 1680
HEIGHT = 1050
class XortexRenderer(Renderer):
def __init__(self):
Renderer.__init__(self)
self.context = EGL(depth_size=0)
self.startTime = time.time()
# Position attributes are in [-1, 1] x [-1, 1]. We can turn these into
# texture coordinates ourselves, so we only take in one vertex attribute.
self.vertexShader = """
attribute vec2 position;
varying vec2 texcoord;
void main() {
gl_Position = vec4(position, 0.0, 1.0);
texcoord = position * 0.5 + 0.5;
}
"""
# Color is determined entirely by the texture.
self.fragmentShader = """
precision mediump float;
uniform sampler2D frame;
varying vec2 texcoord;
void main() {
gl_FragColor = texture2D(frame, texcoord);
}
"""
# The position attribute is in slot 0.
self.binding = ((0, 'position'),)
self.program = self.context.get_program(self.vertexShader, self.fragmentShader, self.binding)
# Fill the viewport.
self.vertices = eglfloats((-1.0, -1.0,
1.0, -1.0,
-1.0, 1.0,
1.0, 1.0))
# The will be uploaded to texture unit 0.
opengles.glUseProgram(self.program)
self.frameTextureUniformID = opengles.glGetUniformLocation(self.program, "frame")
opengles.glUniform1i(self.frameTextureUniformID, 0)
opengles.glUseProgram(0)
# Let's make that texture!
self.frameTextureID = eglint()
opengles.glActiveTexture(GL_TEXTURE0)
opengles.glGenTextures(1, ctypes.byref(self.frameTextureID))
opengles.glBindTexture(GL_TEXTURE_2D, ctypes.byref(self.frameTextureID))
opengles.glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST)
opengles.glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST)
# An RGB image. We'll create a colorful XOR texture, 'cuz it's pretty.
width = 128
height = 128
maxIntensity = max(width, height) - 1.0
vals = [0] * width * height * 3
for r in range(height):
for c in range(width):
intensity = int((r ^ c) / maxIntensity * 255)
vals[(r * width + c) * 3 + 0] = intensity * (r < height / 2)
vals[(r * width + c) * 3 + 1] = intensity * (r > height / 2)
vals[(r * width + c) * 3 + 2] = intensity * (c > height / 2)
# Now we ship the texture off. Bye-bye.
opengles.glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, width, height, 0, GL_RGB, GL_UNSIGNED_BYTE, eglubytes(vals))
self.checkError('after init')
def onKeyEvent(self, keycode):
pass
def onMouseEvent(self, event):
pass
def onDraw(self):
opengles.glViewport(0, 0, self.context.width, self.context.height);
opengles.glClear(GL_COLOR_BUFFER_BIT)
opengles.glUseProgram(self.program)
opengles.glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, 0, self.vertices)
opengles.glEnableVertexAttribArray(0)
opengles.glDrawArrays(GL_TRIANGLE_STRIP, 0, 4)
self.checkError('after draw')
openegl.eglSwapBuffers(self.context.display, self.context.surface)
def checkError(self, msg):
e = opengles.glGetError()
if e:
print msg, 'Error: ', hex(e)
sys.exit(1)
if __name__ == "__main__":
with XortexRenderer() as renderer:
render.goMouseless(WIDTH, HEIGHT, 0.02, renderer)
]]>After installing the following:
sudo apt-get install
gstreamer0.10-plugins-bad
gstreamer0.10-plugins-really-bad
gstreamer0.10-plugins-ugly
gstreamer0.10-ffmpeg
OpenCV was able to to read the .mpg file form the “CaputreFromFile” function.
I then grabbed the frame with “QueryFrame” which returned an iplImage object which has useful information such as width, height and nChannels among other things.
The raw pixel data can be retrieved by calling Get2D and passing in the image and the [x,y] coordinates, which retrieves a pixel. The pixel is an array of 3 (nChannels) different integers which can be accessed like so: pixel[0] pixel[1] pixel[2].
After all that here is the code:
#!/usr/bin/env python
import cv2.cv as cv
import sys
files = sys.argv[1:]
for f in files:
print f
capture = cv.CaptureFromFile(f)
frame = cv.QueryFrame(capture)
width = frame.width
height = frame.height
#just get the first 10 frame for brevity
for i in xrange(10):
frame = cv.QueryFrame(capture)
if frame:
cloned_frame = cv.CloneImage(frame)
print "pixel BGR"
for i in range(0, int(height)):
for j in range(0, int(width)):
pixel = cv.Get2D(frame, i, j)
#in the order: Blue Green Red
print repr(pixel[0]) + ", " + repr(pixel[1]) + ", " + repr(pixel[2])
It prints out all the pixel values, which does take quite some time, but isn’t necessary for any other reason but demonstration.
]]>
To install OpenCV and the Python bindings for it I followed the instructions found on this website.
However I found an error in the last section of code, specifically the “cmake” line, it should read as follows:
cmake -D CMAKE_BUILD_TYPE=RELEASE -D CMAKE_INSTALL_PREFIX=/usr/local -D BUILD_NEW_PYTHON_SUPPORT=ON -D BUILD_EXAMPLES=ON ..
The difference is the”NEW_” between “BUILD_” and “PYTHON”.
Let the “make” command run overnight. It took the two Pies I was running around 12 hours each to compile the libraries. My guess as to why it took so long is because the limited amount of RAM on the devices.
Now to grab a frame! I found this code on a StackOverflow post and looked up the OpenCV function calls to actually figure out what it’s doing.
#!/usr/bin/env python
import cv2.cv as cv
import sys
files = sys.argv[1:]
for f in files:
printf f
capture = cv.CaptureFromFile(f)
print capture
frame = cv. QueryFrame(capture)
print cv.GetCaptureProperty(capture, cv.CV_CAP_PROP_FRAME_WIDTH)
print cv.GetCaptureProperty(capture, cv.CV_CAP_PROP_FRAME_HEIGHT)
for i in xrange(10):
print i
# this frame can be written to a file
frame = cv.QueryFrame(capture)
if frame:
print frame
I downloaded a few different small video files. A .avi, .m4v, .mpg, .mp4
When executed, I ran into the following error:
GLib-GObject-CRITICAL **: g_object_set: assertion 'G_IS_OBJECT (object)' failed]]>
Rectangles will be used for the bouncing ball, the forts, and the ultra-sensitive kings hidden away inside the forts. They also need to be movable and able to be checked for collisions with other rectangles.
class Rectangle:
def __init__(self, l, r, b, t):
self.l = l
self.r = r
self.b = b
self.t = t
def collidesWith(self, other):
return not (self.r < other.l or self.l > other.r or self.t < other.b or self.b > other.t)
def getVertices(self):
return [self.l, self.b, 0.0,
self.r, self.b, 0.0,
self.l, self.t, 0.0,
self.r, self.t, 0.0]
def translate(self, dx, dy):
self.l += dx
self.r += dx
self.b += dy
self.t += dy
This is the obligatory first step to any game.
The code behind it is a modification of our previous OpenGL renderers:
#!/usr/bin/env python
import time
import sys
from pyopengles import *
from geometry import *
from glop import *
WIDTH = 1680
HEIGHT = 1050
ASPECT = WIDTH / float(HEIGHT)
class FourLordsRenderer(Renderer):
def __init__(self):
Renderer.__init__(self)
self.context = EGL(depth_size=8)
self.startTime = time.time()
self.vertexShader = """
uniform mat4 projection;
uniform mat4 modelview;
attribute vec3 position;
attribute vec3 normal;
void main() {
gl_Position = projection * modelview * vec4(position, 1.0);
}
"""
self.fragmentShader = """
precision mediump float;
uniform float time;
uniform vec2 resolution;
void main() {
gl_FragColor = vec4(vec3(0), 1.0);
}
"""
self.binding = ((0, 'position'),)
self.program = self.context.get_program(self.vertexShader, self.fragmentShader, self.binding)
opengles.glUseProgram(self.program)
self.modelviewUID = opengles.glGetUniformLocation(self.program, "modelview")
self.projectionUID = opengles.glGetUniformLocation(self.program, "projection")
self.resolutionUID = opengles.glGetUniformLocation(self.program, "resolution")
self.timeUID = opengles.glGetUniformLocation(self.program, "time")
opengles.glUseProgram(0)
opengles.glUseProgram(self.program)
opengles.glUniform2f(self.resolutionUID, eglfloat(WIDTH), eglfloat(HEIGHT))
opengles.glUseProgram(0)
opengles.glClearColor(eglfloat(0.85), eglfloat(0.35), eglfloat(0.15), eglfloat(1.0))
# Let's make a new rectangle centered in the viewport with a random velocity.
self.rectangle = Rectangle(-0.1, 0.1, -0.1, 0.1)
self.velocity = Vector2.getRandom(1.0)
self.vertices = eglfloats(self.rectangle.getVertices())
self.checkError('after init')
self.modelview = Matrix4.getIdentity()
# Let's always show at least [-1, 1] x [-1, 1]. If our aspect ratio gives
# us more real estate than that on either x or y, let's increase the span
# shown on that axis.
# If we have more height than width, show more on the y-axis.
if ASPECT < 1.0:
self.right = 1.0
self.top = 1.0 / ASPECT
# Otherwise, show more of the x-axis.
else:
self.right = 1.0 * ASPECT
self.top = 1.0
self.left = -self.right
self.bottom = -self.top
self.projection = Matrix4.getOrthoMatrix(self.left, self.right, self.bottom, self.top, -50.0, 50.0)
def onKeyEvent(self, keycode):
pass
def onMouseEvent(self, event):
pass
def onDraw(self):
opengles.glViewport(0, 0, self.context.width, self.context.height);
opengles.glClear(GL_COLOR_BUFFER_BIT)
opengles.glUseProgram(self.program)
opengles.glUniform1f(self.timeUID, eglfloat(time.time() - self.startTime))
opengles.glUniformMatrix4fv(self.modelviewUID, 1, GL_FALSE, eglfloats(self.modelview.data))
opengles.glUniformMatrix4fv(self.projectionUID, 1, GL_FALSE, eglfloats(self.projection.data))
opengles.glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 0, self.vertices)
opengles.glEnableVertexAttribArray(0)
opengles.glDrawArrays(GL_TRIANGLE_STRIP, 0, 4)
opengles.glUseProgram(0)
self.checkError('after draw')
openegl.eglSwapBuffers(self.context.display, self.context.surface)
# Update rectangle's position based on its velocity and the time elapsed
# since the last frame.
self.rectangle.translate(self.deltaTime * self.velocity[0], self.deltaTime * self.velocity[1])
self.vertices = eglfloats(self.rectangle.getVertices())
# If we exceed the bounds of the visible space, let's reflect the velocity
# vector about the normal of the boundary we crossed.
if self.rectangle.l < self.left or self.rectangle.r > self.right:
self.velocity[0] = -self.velocity[0]
if self.rectangle.b < self.bottom or self.rectangle.t > self.top:
self.velocity[1] = -self.velocity[1]
def checkError(self, msg):
e = opengles.glGetError()
if e:
print msg, 'Error: ', hex(e)
sys.exit(1)
if __name__ == "__main__":
with FourLordsRenderer() as renderer:
render.go(WIDTH, HEIGHT, 0.02, renderer)
To store velocity, I also created a simple Vector2 class:
import math
import random
class Vector2:
def __init__(self, x = 0.0, y = 0.0):
self.coords = [x, y]
def getLength(self):
return math.sqrt(self.coords[0] * self.coords[0] + self.coords[1] * self.coords[1])
def normalize(self):
length = self.getLength()
if length > 0.0:
self.coords[0] /= length
self.coords[1] /= length
def __getitem__(self, i):
return self.coords[i]
def __setitem__(self, i, value):
self.coords[i] = value
def __str__(self):
return repr(self.coords)
@staticmethod
def getRandom(magnitude = 1.0):
angle = random.random() * 2.0 * math.pi
return Vector2(math.cos(angle) * magnitude, math.sin(angle) * magnitude)
I’m finding that I do not appreciate Python’s mandatory requirements of including the invoking object in methods’ parameter lists and qualifying instance variable references with that object. Python provides a very thin veneer of object-orientation. The customary notation is only seen on method calls. Everywhere else methods look just like functions taking in structs. It doesn’t capitalize on the context benefits that a class provides. Most of my variable accesses are going to be to instance variables. Can’t we make the most common thing the easiest thing? Outlaw shadows of instance variables, require qualification for access to globals, and let’s eliminate qualified access to instance variables. I feel like I’m writing C code, not object-oriented code.
The next step is to write a renderer that bounces the ball around a field strewn with randomly-placed rectangles, which disappear when collided with. This one’s not done yet. I need to think about the best way to dynamically remove elements from the vertex array that won’t impose a linear time operation to reshuffle the array’s contents. Probably swapping out the last item in the list for the one collided with will work.
This is work for another day.
]]>The malignancy is gone. I did not have a depth buffer in my EGL context. EGL(depth_size=8) fixed that.
I also wanted to be able to handle key events. Peter insisted on a select-driven event loop, so I went searching to figure out how to read the keyboard via a file descriptor. Termios needed to be called upon to get immediate key events, and I ran into the problem of the ages. Most keys are just one byte, but cursor keys are codified with a 3-byte sequence. Up, for example, is sent as <Esc>-[-A. This meant I needed to read more than one byte—if there was more than one to read. My first approach failed: I read 1 byte and then called select() with a timeout of 0. It never reported stdin as having more input to read, even when I pressed multibyte keys. Then I tried reading multiple bytes at a time with file.read. That blocked on normal keys. Finally I found a solution that used os.read(fd, nbytes) instead of file.read(nbytes). That seems to work. I don’t know why os.read is different.
This person had what sounds like the same problem.
After a good amount of trial and error, this is the resulting Keyboard class:
#!/usr/bin/env python
import os
import select
import sys
import termios
class Keyboard:
ESCAPE = 27
LEFT = 1000
RIGHT = 1001
DOWN = 1002
UP = 1003
keylist = {
'\x1b' : ESCAPE,
'\x1b[A' : UP,
'\x1b[B' : DOWN,
'\x1b[C' : RIGHT,
'\x1b[D' : LEFT,
}
def __init__(self):
self.fd = sys.stdin.fileno()
self.old = termios.tcgetattr(self.fd)
self.new = termios.tcgetattr(self.fd)
self.new[3] = self.new[3] & ~termios.ICANON & ~termios.ECHO
self.new[6][termios.VMIN] = 1
self.new[6][termios.VTIME] = 0
termios.tcsetattr(self.fd, termios.TCSANOW, self.new)
def __enter__(self):
return self
def __exit__(self, type, value, traceback):
termios.tcsetattr(self.fd, termios.TCSAFLUSH, self.old)
def getFile(self):
return self.fd
def read(self):
keys = os.read(self.fd, 4)
if keys in Keyboard.keylist:
return Keyboard.keylist[keys]
else:
return None
if __name__ == "__main__":
with Keyboard() as keyboard:
key = keyboard.read()
while key != Keyboard.ESCAPE:
print '%d' % key
key = keyboard.read()
I really like Python’s with statement.
My rendering had to change to accommodate a second source of events:
#!/usr/bin/env python
import select
from mouse import Mouse
from keyboard import Keyboard
from mouseevent import MouseEvent
class Renderer:
def onMouseEvent(self, event):
print event
def onKeyEvent(self, keycode):
pass
def onDraw(self):
print 'drawing'
def __enter__(self):
return self
def __exit__(self, type, value, traceback):
print 'clean up'
def go(width, height, secondsPerFrame, renderer):
with Mouse(width, height) as mouse, Keyboard() as keyboard:
keycode = 0
while keycode != Keyboard.ESCAPE:
toReads, _, _ = select.select([mouse.getFile(), keyboard.getFile()], [], [], secondsPerFrame)
for toRead in toReads:
if toRead is mouse.getFile():
event = mouse.update()
renderer.onMouseEvent(event)
elif toRead is keyboard.getFile():
keycode = keyboard.read()
renderer.onKeyEvent(keycode)
renderer.onDraw()
if __name__ == "__main__":
with Renderer() as renderer:
go(640, 480, 3.0, renderer)
To allow for rotations and to get a proper projection matrix in OpenGL, I whipped up a minimal Matrix4 class:
import math
class Matrix4:
@staticmethod
def getRotationMatrix(degrees, axis):
radians = degrees * math.pi / 180.0
sine = math.sin(radians)
cosine = math.cos(radians)
cosineComplement = 1.0 - cosine
m = Matrix4()
m[0, 0] = cosineComplement * axis[0] * axis[0] + cosine
m[0, 1] = cosineComplement * axis[0] * axis[1] - sine * axis[2]
m[0, 2] = cosineComplement * axis[0] * axis[2] + sine * axis[1]
m[1, 0] = cosineComplement * axis[1] * axis[0] + sine * axis[2]
m[1, 1] = cosineComplement * axis[1] * axis[1] + cosine
m[1, 2] = cosineComplement * axis[1] * axis[2] + sine * axis[0]
m[2, 0] = cosineComplement * axis[2] * axis[0] - sine * axis[1]
m[2, 1] = cosineComplement * axis[2] * axis[1] + sine * axis[0]
m[2, 2] = cosineComplement * axis[2] * axis[2] + cosine
m[3, 3] = 1
return m
@staticmethod
def getOrthoMatrix(l, r, b, t, n, f):
m = Matrix4()
m[0, 0] = 2.0 / (r - l)
m[1, 1] = 2.0 / (t - b)
m[2, 2] = 2.0 / (f - n)
m[0, 3] = -(r + l) / (r - l)
m[1, 3] = -(t + b) / (t - b)
m[2, 3] = -(f + n) / (f - n)
m[3, 3] = 1.0
return m
def __init__(self):
self.data = [0] * 16
def __getitem__(self, rc):
r, c = rc
return self.data[r + c * 4]
def __setitem__(self, rc, value):
r, c = rc
self.data[r + c * 4] = value
The auto-tupling of r, c in Python was new to me. To allow for multi-index subscripting into a matrix, the row and column get tuplefied and passed in to the __getitem__ and __setitem__ methods as a collective. I decompose that tuple into its separate components. The matrix is serialized as a list in column-major order. OpenGL expects matrix data in columns, not rows.
Finally, I was able to pull all this together into a working mesh renderer:
from pyopengles import *
import render
import time
import sys
from render import Renderer
from eric import TriMesh
from keyboard import Keyboard
from matrix import Matrix4
print sys.argv
WIDTH = 1680
HEIGHT = 1050
ASPECT = WIDTH / float(HEIGHT)
class TriMoveRenderer(Renderer):
def __init__(self):
self.context = EGL(depth_size=8)
self.startTime = time.time()
self.vertexShader = """
uniform mat4 projection;
uniform mat4 modelview;
attribute vec3 position;
attribute vec3 normal;
varying vec3 fnormal;
void main() {
gl_Position = projection * modelview * vec4(position, 1.0);
fnormal = (modelview * vec4(normal, 0.0)).xyz;
}
"""
self.fragmentShader = """
precision mediump float;
uniform float time;
uniform vec2 mouse;
uniform vec2 resolution;
const vec3 albedo = vec3(1.0);
const float ambientStrength = 0.1;
const float diffuseStrength = 1.0 - ambientStrength;
const vec3 lightDirection = normalize(vec3(5.0, 5.0, -5.0));
const vec3 eyeDirection = vec3(0.0, 0.0, -1.0);
const float shininess = 0.0;
varying vec3 fnormal;
void main() {
vec3 normal = normalize(fnormal);
vec3 ambient = ambientStrength * albedo;
float nDotL = max(dot(normal, lightDirection), 0.0);
vec3 diffuse = diffuseStrength * nDotL * albedo;
vec3 halfVector = normalize(eyeDirection + lightDirection);
float nDotH = max(dot(normal, halfVector), 0.0);
vec3 specular = vec3(0.0); //pow(nDotH, cos(shininess)) * vec3(1.0);
vec3 color = ambient + diffuse + specular;
gl_FragColor = vec4(color, 1.0);
// gl_FragColor = vec4(normal, 1.0);
}
"""
self.binding = ((0, 'position'),
(1, 'normal'),)
self.program = self.context.get_program(self.vertexShader, self.fragmentShader, self.binding)
opengles.glUseProgram(self.program)
self.modelviewUID = opengles.glGetUniformLocation(self.program, "modelview")
self.projectionUID = opengles.glGetUniformLocation(self.program, "projection")
self.mouseUID = opengles.glGetUniformLocation(self.program, "mouse")
self.resolutionUID = opengles.glGetUniformLocation(self.program, "resolution")
self.timeUID = opengles.glGetUniformLocation(self.program, "time")
opengles.glUseProgram(0)
opengles.glUseProgram(self.program)
opengles.glUniform2f(self.resolutionUID, eglfloat(WIDTH), eglfloat(HEIGHT))
opengles.glUniform2f(self.mouseUID, eglfloat(0.5), eglfloat(0.5))
opengles.glUseProgram(0)
opengles.glClearColor(eglfloat(0.45), eglfloat(0.35), eglfloat(0.15), eglfloat(1.0))
mesh = TriMesh(sys.argv[1])
self.vertices = eglfloats(mesh.verts)
self.normals = eglfloats(mesh.vNorms)
self.faces = eglshorts(mesh.faces)
self.checkError('after init')
opengles.glClearDepthf(eglfloat(1.0))
opengles.glEnable(GL_DEPTH_TEST)
opengles.glEnable(GL_CULL_FACE)
opengles.glFrontFace(GL_CW)
self.yAngle = 0.0
self.rotation = Matrix4.getRotationMatrix(self.yAngle, [0.0, 1.0, 0.0])
if ASPECT < 1.0:
self.projection = Matrix4.getOrthoMatrix(-1.0, 1.0, -1.0 / ASPECT, 1.0 / ASPECT, -50.0, 50.0)
else:
self.projection = Matrix4.getOrthoMatrix(-1.0 * ASPECT, 1.0 * ASPECT, -1.0, 1.0, -50.0, 50.0)
def onKeyEvent(self, keycode):
if keycode == Keyboard.LEFT:
self.yAngle -= 4
self.rotation = Matrix4.getRotationMatrix(self.yAngle, [0.0, 1.0, 0.0])
elif keycode == Keyboard.RIGHT:
self.yAngle += 4
self.rotation = Matrix4.getRotationMatrix(self.yAngle, [0.0, 1.0, 0.0])
def onMouseEvent(self, event):
opengles.glUseProgram(self.program)
opengles.glUniform2f(self.mouseUID, eglfloat(event.x / float(WIDTH)), eglfloat(event.y / float(HEIGHT)))
opengles.glUseProgram(0)
def onDraw(self):
opengles.glViewport(0, 0, self.context.width, self.context.height);
opengles.glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT)
opengles.glPointSize(eglfloat(5.0))
self.checkError('before use program')
opengles.glUseProgram(self.program)
opengles.glUniform2f(self.resolutionUID, eglfloat(WIDTH), eglfloat(HEIGHT))
opengles.glUniform1f(self.timeUID, eglfloat(time.time() - self.startTime))
opengles.glUniformMatrix4fv(self.modelviewUID, 1, GL_FALSE, eglfloats(self.rotation.data))
opengles.glUniformMatrix4fv(self.projectionUID, 1, GL_FALSE, eglfloats(self.projection.data))
opengles.glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 0, self.vertices)
opengles.glEnableVertexAttribArray(0)
opengles.glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, 0, self.normals)
opengles.glEnableVertexAttribArray(1)
opengles.glDrawElements(GL_TRIANGLES, len(self.faces), GL_UNSIGNED_SHORT, self.faces)
opengles.glUseProgram(0)
self.checkError('after draw')
openegl.eglSwapBuffers(self.context.display, self.context.surface)
def checkError(self, msg):
e = opengles.glGetError()
if e:
print msg, 'Error: ', hex(e)
sys.exit(1)
if __name__ == "__main__":
with TriMoveRenderer() as renderer:
render.go(WIDTH, HEIGHT, 0.02, renderer)
Face winding is clockwise here because I like a different handedness than the rest of the world.
The result? Breaktaking.
There’s a triangle missing from Al’s chin. I’m not sure why.
]]>We’re looking at Al’s backside, but we’re seeing through some of him. It looks like my depth test isn’t working. That’s work for another day.
Our test renderer looks like this:
from pyopengles import *
from render import Renderer
from eric import TriMesh
import render
import time
import sys
WIDTH = 1680
HEIGHT = 1050
class TriMoveRenderer(Renderer):
def __init__(self):
self.context = EGL()
self.startTime = time.time()
self.vertexShader = """
attribute vec3 position;
attribute vec3 normal;
varying vec3 fnormal;
void main() {
gl_Position = vec4(position, 1.0);
fnormal = normal;
}
"""
self.fragmentShader = """
precision mediump float;
uniform float time;
uniform vec2 mouse;
uniform vec2 resolution;
const vec3 albedo = vec3(1.0);
const float ambientStrength = 0.1;
const float diffuseStrength = 1.0 - ambientStrength;
const vec3 lightDirection = normalize(vec3(5.0, 5.0, -5.0));
const vec3 eyeDirection = vec3(0.0, 0.0, -1.0);
const float shininess = 50.0;
varying vec3 fnormal;
void main() {
vec3 normal = normalize(fnormal);
vec3 ambient = ambientStrength * albedo;
float nDotL = dot(normal, lightDirection);
vec3 diffuse = diffuseStrength * nDotL * albedo;
vec3 halfVector = normalize(eyeDirection + lightDirection);
float nDotH = dot(normal, halfVector);
vec3 specular = pow(nDotH, cos(shininess)) * vec3(1.0);
vec3 color = ambient + diffuse + specular;
gl_FragColor = vec4(color, 1.0);
}
"""
self.binding = ((0, 'position'),
(1, 'normal'),)
self.program = self.context.get_program(self.vertexShader, self.fragmentShader, self.binding)
opengles.glUseProgram(self.program)
self.mouseUID = opengles.glGetUniformLocation(self.program, "mouse")
self.resolutionUID = opengles.glGetUniformLocation(self.program, "resolution")
self.timeUID = opengles.glGetUniformLocation(self.program, "time")
opengles.glUseProgram(0)
opengles.glUseProgram(self.program)
opengles.glUniform2f(self.resolutionUID, eglfloat(WIDTH), eglfloat(HEIGHT))
opengles.glUniform2f(self.mouseUID, eglfloat(0.5), eglfloat(0.5))
opengles.glUseProgram(0)
opengles.glClearColor(eglfloat(0.45), eglfloat(0.35), eglfloat(0.15), eglfloat(1.0))
mesh = TriMesh('../al.obj')
self.vertices = eglfloats(mesh.verts)
self.normals = eglfloats(mesh.vNorms)
self.faces = eglshorts(mesh.faces)
self.checkError('after init')
opengles.glEnable(GL_DEPTH_TEST)
def onMouseEvent(self, event):
opengles.glUseProgram(self.program)
opengles.glUniform2f(self.mouseUID, eglfloat(event.x / float(WIDTH)), eglfloat(event.y / float(HEIGHT)))
opengles.glUseProgram(0)
def onDraw(self):
opengles.glViewport(0, 0, self.context.width, self.context.height);
opengles.glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT)
opengles.glPointSize(eglfloat(5.0))
self.checkError('before use program')
opengles.glUseProgram(self.program)
opengles.glUniform2f(self.resolutionUID, eglfloat(WIDTH), eglfloat(HEIGHT))
opengles.glUniform1f(self.timeUID, eglfloat(time.time() - self.startTime))
opengles.glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 0, self.vertices)
opengles.glEnableVertexAttribArray(0)
opengles.glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, 0, self.normals)
opengles.glEnableVertexAttribArray(1)
opengles.glDrawElements(GL_TRIANGLES, len(self.faces), GL_UNSIGNED_SHORT, self.faces)
opengles.glUseProgram(0)
self.checkError('after draw')
openegl.eglSwapBuffers(self.context.display, self.context.surface)
def checkError(self, msg):
e = opengles.glGetError()
if e:
print msg, 'Error: ', hex(e)
sys.exit(1)
if __name__ == "__main__":
with TriMoveRenderer() as renderer:
render.go(WIDTH, HEIGHT, 0.02, renderer)
I added an eglshorts to pyopengles, as glDrawElements wouldn’t accept ints. I also learned how to make a package in Python: make a directory named with your package name and add an __init__.py file inside of it. I made a package named eric and dropped TriMesh.py inside.
]]>Since we want to run some nifty graphics demos on our raspberry pi’s/ raspberries pi (no sure how to pluralize this), we thought it would be handy to have a way to loading some models. The representation we decided on was the Wavefront OBJ, as it is very popular.
Deciding that we only needed the most basic features, we limited the specs so that only triangles are accepted. Also, we only wanted to read in the verticies and the faces, leaving the calculation of the normals up to the loader.
Here it (slightly reformatted to fit nicely):
import re
class TriMesh:
def __init__(self, fileName):
self.verts = []
self.faces = []
self.fNorms = []
self.vNorms = []
# read file
with open(fileName) as f:
for line in f:
token = re.split('\s*', line.strip())
if token[0] == 'v':
self.verts.extend(
[float(token[i]) for i in range(1,4)])
elif line[0] == 'f':
self.faces.extend(
[int(token[i]) - 1 for i in range(1,4)])
#initialize the vertex normals
self.vNorms = [0] * len(self.verts)
x,y,z = 0,1,2
for i in xrange(0, len(self.faces), 3):
#get the index of the x coordinate for the three points
A = self.faces[i] * 3
B = self.faces[i+1] * 3
C = self.faces[i+2] * 3
#create two vectors from the three points of the triangle
vBA = (self.verts[B] - self.verts[A],
self.verts[B+1] - self.verts[A+1],
self.verts[B+2] - self.verts[A+2])
vCA = (self.verts[C] - self.verts[A],
self.verts[C+1] - self.verts[A+1],
self.verts[C+2] - self.verts[A+2])
#do the cross product to get the face normal
normal = (vBA[y] * vCA[z] - vBA[z] * vCA[y],
vBA[z] * vCA[x] - vBA[x] * vCA[z],
vBA[x] * vCA[y] - vBA[y] * vCA[x])
#unitize the face normal
magnitude = (normal[x]**2 + normal[y]**2 + normal[z]**2)**.5
#dont divide by zero
if magnitude != 0:
unitNormal = (normal[x] / magnitude,
normal[y] / magnitude,
normal[z] / magnitude)
else:
unitNormal = (0.0,0.0,0.0)
#incrementally calculate vertex normals
for j in range(3):
self.vNorms[A + j] += unitNormal[j]
self.vNorms[B + j] += unitNormal[j]
self.vNorms[C + j] += unitNormal[j]
self.fNorms.extend(unitNormal)
#unitize vertex normals
for i in xrange(0, len(self.vNorms), 3):
magnitude = ( self.vNorms[i]**2 +
self.vNorms[i+1]**2 +
self.vNorms[i+2]**2)**.5
if magnitude != 0:
self.vNorms[i] /= magnitude
self.vNorms[i+1] /= magnitude
self.vNorms[i+2] /= magnitude
def printObj(self):
tmp = ''
for i in xrange(0, len(self.verts), 3):
tmp += "v " + str(self.verts[i])
tmp += " " + str(self.verts[i+1])
tmp += " " + str(self.verts[i+2]) + "\n"
tmp += "\n"
for i in xrange(0, len(self.vNorms), 3):
tmp += "vn "+ str(self.vNorms[i])
tmp += " " + str(self.vNorms[i+1])
tmp += " " + str(self.vNorms[i+2]) + "\n"
tmp += "\n"
for i in xrange(0, len(self.faces), 3):
tmp += "f " + str(self.faces[i] + 1)
tmp += " " + str(self.faces[i+1] + 1)
tmp += " " + str(self.faces[i+2] + 1) + "\n"
tmp += "\n"
print tmp
The title refers to the fact that I have also wrote a version that is much more object oriented, but never quite managed to finish it.
Here’s a render, in Blender, of a teapot loaded in from a OBJ:

The OBJ I used to load this,along with quite a few others, can be found at http://people.sc.fsu.edu/~jburkardt/data/obj/obj.html (the link tag keeps getting deleted for some reason)
]]>The reason I wasn’t successful is this: When I executed Dr. Johnson’s triangle.py program, the program would give me the following error:
numconfig= c_long(1)
Traceback (most recent call last):
File "triangle.py", line 27, in <module>
program = context.get_program(vertexShader, fragmentShader, binding)
AttributeError: 'EGL' object has no attribute 'get_program'
This is after installing the pyopengles library. Googling provided little help.
One lesson I reminded myself of is that “Order matters” order referring to the sequence of things. I spent all too much time trying to get Dr. Bui’s barrier.py code up and running again with both our machines on my local network. I was trying this command:
mpiexec -np 2 python barrier.py -f machines
Instead of:
mpiexec -np 2 -f machines python barrier.py
It just putting the program to execute at the end made the difference.
The next thing I accomplished was some analysis on the delay it took between when the initial board would write its output and the other boards to receive the message and write their own. If this time is too long, they videos will not look as if they are playing at the same time.
Taking the log files generated from Dr. Bui’s barrier.py program, here are the results of analyzing the difference in time between when bramble00 and bramble01 wrote the same line to their log files. There are 736 samples. All values are in seconds:
| avg | min | max | range | std dev |
| 0.009541388 | 0.008450031 | 0.011140108 | 0.00269 | 0.000529 |
]]>
Normally I’d use a windowing toolkit like wxWidgets or SDL to get mouse events. But I’m not even running an X server on my Raspberry Pi! I looked at the demos in pyopengles and found that they could run without X and still support mouse interaction. Having grown up on supporting libraries, this was not a world I was familiar with. I found the details in pymouse.py.
To get mouse interaction in my demo, I followed this progression:
And it all worked!
Here’s my crude mouse listener:
#!/usr/bin/env python
import threading
import time
class MouseListener(threading.Thread):
def __init__(self):
threading.Thread.__init__(self)
self.fd = open('/dev/input/mouse0', 'r')
self.height = 1080
self.width = 1920
self.x = self.height / 2
self.y = self.width / 2
self.finished = False
def run(self):
while 1:
while 1:
buttons, dx, dy = map(ord, self.fd.read(3))
# Bit 3 is always set. Always! Move on.
if buttons & (1 << 3):
break
# Maybe not always? Maybe a read failed and bit 3 wasn't set.
# The original author suggests we try to sync up again with a
# read of one byte. Why only one?
self.fd.read(1)
if buttons & (1 << 1) and buttons & (1 << 0):
print 'both pressed'
self.finished = True
break
elif buttons & (1 << 2):
print 'middle press'
elif buttons & (1 << 1):
print 'right press'
elif buttons & (1 << 0):
print 'left press'
# Going left
if buttons & (1 << 4):
dx -= 256
# Going right
if buttons & (1 << 5):
dy -= 256
self.x += dx
self.y += dy
if self.x < 0:
self.x = 0
elif self.x > self.width:
self.x = self.width
if self.y < 0:
self.y = 0
elif self.y > self.height:
self.y = self.height
def startMouse():
thread = MouseListener()
thread.start()
return thread
startMouse()
]]>