Mittwoch, 29. April 2009

Basic Shading





The following script demonstrates basic shading. A Vertex Shader and a Fragment (Pixel) Shader are defined. Vertex shaders are necissary to transfer important data to the Pixel shader which isn't accessible otherwise. A display list is used to keep the number of calls low. A (moving) light source position is transferred from python to GLSL via the GL_LIGHT0 variables.

I think it is curious to call the (implicit) cosine (in GLSL "dot(L,N)") "Lambert's law", which is occasionally the case. Lambert's law states that a glowing surface will look the same from every angle. This is implicit in that the color of a fragment will not change depending on the viewing angle (except if you specify it to do so). The point in this dot product is that a face will appear dimmer when struck by light at an acute angle, because it will span less of a solid angle viewed from the light source. This is not the opposite of Lambert's law or something.


# Pygame/PyopenGL example by Bastiaan Zapf, Apr 2009
#
# Draw a range of cubes, light them prettily
#
# Employed techniques:
#
# - Vertex and Fragment shaders
# - Lightsource variables
# - Display Lists


from OpenGL.GL import *
from OpenGL.GLU import *
import random
from math import * # trigonometry

import pygame # just to get a display

import Image
import sys
import time

# get an OpenGL surface

pygame.init()
pygame.display.set_mode((800,600), pygame.OPENGL|pygame.DOUBLEBUF)

def jpg_file_write(name, number, data):
im = Image.frombuffer("RGBA", (800,600), data, "raw", "RGBA", 0, 0)
fnumber = "%05d" % number
im.save(name + fnumber + ".jpg")


glEnable(GL_DEPTH_TEST)

# Create and Compile a shader
# but fail with a meaningful message if something goes wrong

def createAndCompileShader(type,source):
shader=glCreateShader(type)
glShaderSource(shader,source)
glCompileShader(shader)

# get "compile status" - glCompileShader will not fail with
# an exception in case of syntax errors

result=glGetShaderiv(shader,GL_COMPILE_STATUS)

if (result!=1): # shader didn't compile
raise Exception("Couldn't compile shader\nShader compilation Log:\n"+glGetShaderInfoLog(shader))
return shader

# Create and Compile fragment and vertex shaders
# Transfer data from fragment to vertex shader

vertex_shader=createAndCompileShader(GL_VERTEX_SHADER,"""
varying vec3 v;
varying vec3 N;

void main(void)
{

v = gl_ModelViewMatrix * gl_Vertex;
N = gl_NormalMatrix * gl_Normal;

gl_Position = gl_ModelViewProjectionMatrix * gl_Vertex;

}
""");

fragment_shader=createAndCompileShader(GL_FRAGMENT_SHADER,"""
varying vec3 N;
varying vec3 v;

void main(void)
{
vec3 L = gl_LightSource[0].position.xyz-v;

// "Lambert's law"? (see notes)
// Rather: faces will appear dimmer when struck in an acute angle
// distance attenuation

float Idiff = max(dot(normalize(L),N),0.0)*pow(length(L),-2.0);

gl_FragColor = vec4(0.5,0,0.5,1.0)+ // purple
vec4(1.0,1.0,1.0,1.0)*Idiff; // diffuse reflection
}
""");

# build shader program

program=glCreateProgram()
glAttachShader(program,vertex_shader)
glAttachShader(program,fragment_shader)
glLinkProgram(program)

# try to activate/enable shader program
# handle errors wisely

try:
glUseProgram(program)
except OpenGL.error.GLError:
print glGetProgramInfoLog(program)
raise

done = False

t=0

# load a cube into a display list

glNewList(1,GL_COMPILE)

glBegin(GL_QUADS)

glColor3f(1,1,1)

glNormal3f(0,0,-1)
glVertex3f( -1, -1, -1)
glVertex3f( 1, -1, -1)
glVertex3f( 1, 1, -1)
glVertex3f( -1, 1, -1)

glNormal3f(0,0,1)
glVertex3f( -1, -1, 1)
glVertex3f( 1, -1, 1)
glVertex3f( 1, 1, 1)
glVertex3f( -1, 1, 1)

glNormal3f(0,-1,0)
glVertex3f( -1, -1, -1)
glVertex3f( 1, -1, -1)
glVertex3f( 1, -1, 1)
glVertex3f( -1, -1, 1)

glNormal3f(0,1,0)
glVertex3f( -1, 1, -1)
glVertex3f( 1, 1, -1)
glVertex3f( 1, 1, 1)
glVertex3f( -1, 1, 1)

glNormal3f(-1,0,0)
glVertex3f( -1, -1, -1)
glVertex3f( -1, 1, -1)
glVertex3f( -1, 1, 1)
glVertex3f( -1, -1, 1)

glNormal3f(1,0,0)
glVertex3f( 1, -1, -1)
glVertex3f( 1, 1, -1)
glVertex3f( 1, 1, 1)
glVertex3f( 1, -1, 1)

glEnd()
glEndList()

while not done:

t=t+1

glMatrixMode(GL_PROJECTION)
glLoadIdentity()
gluPerspective(90,1,0.01,1000)
gluLookAt(sin(t/260.0)*4,cos(t/260.0)*4,cos(t/687.0)*3,0,0,0,0,1,0)

glClearColor(0.0, 0.0, 0.0, 1.0)
glClear(GL_COLOR_BUFFER_BIT|GL_DEPTH_BUFFER_BIT)

glMatrixMode(GL_MODELVIEW)

# calculate light source position

ld=[sin(t/16.0)*4.0,sin(t/20.0)*4.0,cos(t/16.0)*4.0]

# pass data to fragment shader

glLightfv(GL_LIGHT0,GL_POSITION,[ld[0],ld[1],ld[2]]);

# fallback

glColor3f(1,1,1)

glLoadIdentity()
# render a pretty range of cubes

for i in range(-5,5):
for j in range(-5,5):
for k in range(-5,5):
glPushMatrix()

glTranslate(i,j,k)
glScale(0.1,0.1,0.1)
glCallList(1)
glPopMatrix()

pygame.display.flip()

Samstag, 11. April 2009

Render to Texture





This Program demonstrates a "render to texture" effect. It uses the "Framebuffer Management"-Technique, which has superseded the "Render to Back buffer, GlCopyImage2D, Overwrite Back buffer"-Technique for a few years now.

# Pygame/PyopenGL example by Bastiaan Zapf, Apr 2009
#
# "Render to Texture" demonstration: Render a helix to a
# texture and project it onto the faces of a cube pattern.
#


from OpenGL.GL import *
from OpenGL.GLU import *

from OpenGL.GL.ARB.framebuffer_object import *
from OpenGL.GL.EXT.framebuffer_object import *

from ctypes import *

from math import *
import pygame

pygame.init()

pygame.display.set_mode((800,600), pygame.OPENGL|pygame.DOUBLEBUF)

glClearColor(0.0, 0.0, 0.0, 1.0)
glClear(GL_COLOR_BUFFER_BIT|GL_DEPTH_BUFFER_BIT)

glMatrixMode(GL_MODELVIEW);

done = False

t=0

while not done:

# step time

t=t+1

# render a helix (this is similar to the previous example, but
# this time we'll render to a texture)

# initialize projection

glClearColor(0.0, 0.0, 0.0, 1.0)
glClear(GL_COLOR_BUFFER_BIT|GL_DEPTH_BUFFER_BIT)

glMatrixMode(GL_PROJECTION);
glLoadIdentity()

gluPerspective(90,1,0.01,1000)
gluLookAt(sin(t/200.0)*2,sin(t/500.0)*2,cos(t/200.0)*2,0,0,0,0,1,0)
glMatrixMode(GL_MODELVIEW)

# generate the texture we render to, and set parameters

rendertarget=glGenTextures( 1 )

glBindTexture( GL_TEXTURE_2D, rendertarget );
glTexEnvf( GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_MODULATE );
glTexParameterf( GL_TEXTURE_2D, GL_TEXTURE_WRAP_S,
GL_REPEAT);
glTexParameterf( GL_TEXTURE_2D, GL_TEXTURE_WRAP_T,
GL_REPEAT );
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR)
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR)

# occupy 512x512 texture memory

glTexImage2D(GL_TEXTURE_2D, 0,GL_RGBA,512,512,0,GL_RGBA,
GL_UNSIGNED_INT, None)

# This is the interesting part: render-to-texture is initialized here

# generate a "Framebuffer" object and bind it

fbo=c_uint(1) # WTF? Did not find a way to get there easier
# A simple number would always result in a "Segmentation
# Fault" for me
glGenFramebuffers(1,fbo)
glBindFramebufferEXT(GL_FRAMEBUFFER_EXT, fbo);

# render to the texture

glFramebufferTexture2DEXT(GL_FRAMEBUFFER_EXT, GL_COLOR_ATTACHMENT0_EXT,
GL_TEXTURE_2D, rendertarget, 0);

# In case of errors or suspect behaviour, try this:
# print "Framebuffer Status:"
# print glCheckFramebufferStatusEXT(GL_FRAMEBUFFER_EXT);

# Save Viewport configuration

glPushAttrib(GL_VIEWPORT_BIT);

# Align viewport to Texture dimensions - Try rendering
# to different dimensions than the texture has to find out
# about how your hardware handles display pitch

glViewport(0, 0, 512, 512);

glEnable(GL_BLEND);
glBlendFunc (GL_SRC_ALPHA, GL_ONE);

# Black background for the Helix

glClearColor(0.0, 0.0, 0.0, 0.0)
glClear(GL_COLOR_BUFFER_BIT|GL_DEPTH_BUFFER_BIT)

# Fallback to white

glColor4f(1,1,1,1);

# But try a fancy texture

texture=glGenTextures( 1 )

glBindTexture( GL_TEXTURE_2D, texture );
glTexEnvf( GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_MODULATE );
glTexParameterf( GL_TEXTURE_2D, GL_TEXTURE_WRAP_S,
GL_REPEAT);
glTexParameterf( GL_TEXTURE_2D, GL_TEXTURE_WRAP_T,
GL_REPEAT );
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR)
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR)

texdata=[[[1.0,0,0,1],
[1.0,1,1,1],
[0.0,1,0,1],
[1.0,1,1,1]],
[[1.0,0,0,1],
[1.0,0,0,1],
[1.0,0,0,0.5],
[0.0,0,0,1]],
[[0.0,1,0,1],
[0.0,0,0,0],
[0.0,0,1,1],
[0.0,0,0,0]],
[[0.0,0,0,1],
[0.0,0,0,1],
[0.0,0,0,1],
[0.0,0,0,1]]];

glTexImage2Df(GL_TEXTURE_2D, 0,4,0,GL_RGBA,
texdata)

glEnable( GL_TEXTURE_2D );
glBindTexture( GL_TEXTURE_2D, texture );

# The helix

glBegin(GL_TRIANGLE_STRIP);

for i in range(0,100):

r=5.0;
if (i%2==0):
glTexCoord2f(0,i);
glVertex3f( cos(i/r)*1.5, -2.5+i*0.05, sin(i/r)*1.5);
else:
glTexCoord2f(1,i);
glVertex3f( cos(i/r+2)*1.5, -2.5+i*0.05, sin(i/r+2)*1.5);


glEnd();
glPopAttrib(); # Reset viewport to screen format
# do not render to texture anymore - "switch off" rtt
glBindFramebufferEXT(GL_FRAMEBUFFER_EXT, 0);

glDisable( GL_BLEND );
glEnable( GL_TEXTURE_2D );
glEnable( GL_DEPTH_TEST );

glMatrixMode(GL_PROJECTION);
glLoadIdentity()

gluPerspective(90,1,0.01,1000)

gluLookAt(-1.4+sin(t/100.0)*2.5,
-1.4+sin(t/90.0)*0.4,
-1.4+sin(t/100.0+3.14)*0.5,
0,0,0,
0,1,0)

glMatrixMode(GL_MODELVIEW);

# use the rendered texture from above!
glBindTexture( GL_TEXTURE_2D, rendertarget );

glBegin(GL_QUADS)
glColor3f(1,1,1) # fallback

# render a range of half cubes

for x in range(-2,3,2):
for y in range(-2,3,2):
glTexCoord2f(0,0);
glVertex3f(-1-x, -1+x+y, 1-y)
glTexCoord2f(0,1);
glVertex3f(-1-x, 1+x+y, 1-y)
glTexCoord2f(1,1);
glVertex3f(1-x, 1+x+y, 1-y)
glTexCoord2f(1,0);
glVertex3f(1-x, -1+x+y, 1-y)

glTexCoord2f(1,1);
glVertex3f(1-x,-1+x+y, -1-y)
glTexCoord2f(1,0);
glVertex3f(1-x,-1+x+y, 1-y)
glTexCoord2f(0,0);
glVertex3f(1-x,1+x+y, 1-y)
glTexCoord2f(0,1);
glVertex3f(1-x,1+x+y, -1-y)

glTexCoord2f(1,1);
glVertex3f(-1-x,1+x+y, -1-y)
glTexCoord2f(0,1);
glVertex3f(-1-x,1+x+y, 1-y)
glTexCoord2f(0,0);
glVertex3f(1-x,1+x+y, 1-y)
glTexCoord2f(1,0);
glVertex3f(1-x,1+x+y, -1-y)

glEnd()
glFlush()

pygame.display.flip()

# do not leak any mem

glDeleteTextures(texture)
glDeleteTextures(rendertarget)
glDeleteFramebuffers(1,fbo)

Samstag, 4. April 2009

Screenshots from OpenGL

The shortest way I found to bring OpenGL screenshots to the hard drive is using the function "frombuffer" from the "imaging Library" "Image", which understands the same format that OpenGL outputs.

Documentation for Image
Documentation for glReadPixels


screenshot = glReadPixels( 0,0, 800, 600, GL_RGBA, GL_UNSIGNED_BYTE)
im = Image.frombuffer("RGBA", (800,600), screenshot, "raw", "RGBA", 0, 0)
im.save("test.jpg")

Bizarro Error "AttributeError: __class__"

In case of errors of the following form:


$ python test.py
No handlers could be found for logger "OpenGL.arrays.arraydatatype"
Traceback (most recent call last):
File "test.py", line 135, in
jpg_file_write("helix/helix", t, glReadPixels( 0,0, 800, 600, GL_RGBA, GL_UNSIGNED_BYTE))
File "/var/lib/python-support/python2.5/OpenGL/GL/images.py", line 319, in glReadPixels
imageData = arrayType.dataPointer(array)
File "/var/lib/python-support/python2.5/OpenGL/arrays/arraydatatype.py", line 38, in dataPointer
return cls.getHandler(value).dataPointer( value )
File "/var/lib/python-support/python2.5/OpenGL/arrays/formathandler.py", line 17, in __call__
typ = value.__class__
AttributeError: __class__


Try to install the latest PyOpenGL package.

Freitag, 3. April 2009

Rotating Helix





The following script will display a rotating Helix. It depends on both PyGame and PyOpenGL.



It demonstrates:
  • Texturing
  • Blending (Alpha Channel)
  • Projection/Modelview Basics
  • Fun with Trigonometric Functions
  • Animation (Double Buffered)
It also demonstrates missing Optimisation:

  • The Texture does not have to be reloaded every frame. Animation (see comments for Variable "pulse") would be harder, but could be achieved by blending textures (or blending display data, if blending textures isn't possible)
  • The Helix vertex data could be stored in a Display List. However, we'd lose the possibility to modify the Helix. A vertex shader might be able to do that in the Graphics Controller.
  • The shown version might act as a template for coarse benchmarks. (eg. for answering the question "How many fps will I get when i Upload a 512x512 texture every frame?")


I hope someone will find this fun and educating. Read the comments to get some suggestions about what to toy with first. Comments welcome.

# Pygame/PyopenGL example by Bastiaan Zapf, Apr 2009
#
# Draw an helix, wiggle it pleasantly
#
# Keywords: Alpha Blending, Textures, Animation, Double Buffer

from OpenGL.GL import *
from OpenGL.GLU import *

from math import * # trigonometry

import pygame # just to get a display

# get an OpenGL surface

pygame.init()
pygame.display.set_mode((800,600), pygame.OPENGL|pygame.DOUBLEBUF)

# How to catch errors here?

done = False

t=0

while not done:

t=t+1

# for fun comment out these two lines

glClearColor(0.0, 0.0, 0.0, 1.0)
glClear(GL_COLOR_BUFFER_BIT|GL_DEPTH_BUFFER_BIT)

# Get a perspective at the helix

glMatrixMode(GL_PROJECTION);

glLoadIdentity()
gluPerspective(90,1,0.01,1000)
gluLookAt(sin(t/200.0)*3,sin(t/500.0)*3,cos(t/200.0)*3,0,0,0,0,1,0)

# Draw the helix (this ought to be a display list call)

glMatrixMode(GL_MODELVIEW)

# get a texture (this ought not to be inside the inner loop)

texture=glGenTextures( 1 )

glBindTexture( GL_TEXTURE_2D, texture );
glTexEnvf( GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_MODULATE );

# set sane defaults for a plethora of potentially uninitialized
# variables

glTexParameterf( GL_TEXTURE_2D, GL_TEXTURE_WRAP_S,
GL_REPEAT);
glTexParameterf( GL_TEXTURE_2D, GL_TEXTURE_WRAP_T,
GL_REPEAT );
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR)
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR)

# a texture

#pulse = sin(t/30)*0.5+0.5 # try this one
pulse = 0

texdata=[[[0.0,0,1,1],
[0.0,0,0,0],
[0.0,1,0,1],
[0.0,0,0,0]],
[[0.0,0,0,0],
[pulse,pulse,pulse,1],
[pulse,pulse,pulse,1],
[0.0,0,0,0]],
[[0.0,1,0,1],
[1,pulse,pulse,1],
[pulse,pulse,0,1],
[0.0,0,0,0]],
[[0.0,0,0,0],
[0.0,0,0,0],
[0.0,0,0,0],
[0.0,0,0,0]]];

glTexImage2Df(GL_TEXTURE_2D, 0,4,0,GL_RGBA,
texdata)

glEnable(GL_BLEND);
glBlendFunc (GL_SRC_ALPHA, GL_ONE); # XXX Why GL_ONE?
# alternatively:
# glEnable(GL_DEPTH_TEST);

glEnable( GL_TEXTURE_2D );
# use the texture
glBindTexture( GL_TEXTURE_2D, texture );

# vertices & texture data

glBegin(GL_TRIANGLE_STRIP);

for i in range(0,100):

r=5.0 # try other values - integers as well
d=1 # try other values

if (i%2==0):
glTexCoord2f(0,i);
glVertex3f( cos(i/r), -2.5+i*0.05, sin(i/r));
# glVertex3f( cos(i/r)*pulse2, -2.5+i*0.05, sin(i/r)*pulse2);
else:
glTexCoord2f(1,i);
glVertex3f( cos(i/r+3.14), -2.5+i*0.05+d, sin(i/r+3.14));
# glVertex3f( cos(i/r+3.14)*pulse2, -2.5+i*0.05+d+pulse2*1, sin(i/r+3.14)*pulse2);


glEnd();

glFlush()

glDeleteTextures(texture)
pygame.display.flip()