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.

Comments

Leave a Reply

Your email address will not be published. Required fields are marked *