A rotating 3-D model with Raspberry Pi
I left off with a visible but maligned rendering.
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.