一、现代OpenGL:三维图形管线
OpenGL 应用编程接口(API)从固定功能的图形管线转为可编程的图形管线。
简化三维图形管线分为6步:
1、三维几何图形定义(VBO等)
在第一步,通过定义在三维空间中的三角形的顶点,并指定每个顶点相关联的颜色,我们定义了三维几何图形。
2、顶点着色器
接下来,变换这些顶点:第一次变换将这些顶点放在三维空间中,第二次变换将三维坐标投影到二维空间。根据照明等因素,对应顶点的颜色值也在这一步中计算,这在代码中通常称为“顶点着色器”。
3、光栅化
接着,将几何图形“光栅化”(从几何物体转换为像素),
4、片段着色器
针对每个像素,执行另一个名为“片段着色器”的代码块。正如顶点着色器作用于三维顶点,片段着色器作用于光栅化后的二维像素。
5、帧缓冲区操作(深度测试、混合等)
最后,像素经过一系列帧缓冲区操作,其中,它经过“深度缓冲区检验”(检查一个片段是否遮挡另一个)、“混合”(用透明度混合两个片段)以及其他操作,其当前的颜色与帧缓冲区中该位置已有的颜色结合。
6、帧缓冲区
这些变化最终体现在最后的帧缓冲区上,通常显示在屏幕上。
二、几个常用的通用函数和类
这几个常用的通用函数和类是《Python极客项目编程》作者,大虾Mahesh Venkitachalam写的。使用时在程序中使用import glutils导入即可。Git地址:https://github.com/electronut/pp/blob/master/common/glutils.py
1、加载图像作纹理的函数loadTexture(filename)
loadTexture()函数用 Python 图像库(PIL)的 Image 模块读取图像文件。
然后获取 Image 对象的数据,放入 8 位的 numpy 数组,创建一个 OpenGL 纹理对象,这是在 OpenGL 中利用纹理做任何事的先决条件。执行现在你比较熟悉的绑定 texture 对象,这样所有后来纹理相关的设置都应用于该对象。将数据的拆包对齐设置为 1,这意味着该图像数据被硬件认为是 1 字节(或 8 位)的数据。
告诉 OpenGL 如何处理边缘的纹理。在这个例子中,在几何图形的边缘截取纹理颜色(指定纹理坐标时,惯例是使用字母 S 和 T 表示轴,而不是 x 和 y)。
指定插值类型,在拉伸或压缩纹理来覆盖多边形时采用。在这个例子中,指定为“线性滤波”。
设置绑定纹理中的图像数据。此时,图像数据传送到显存,纹理准备好使用了。
2、生成透视矩阵的函数perspective()
fov:角度;aspect:长宽比;zNear:近点;zFar:远点
math.radians(45.0) :将45度转换成弧度。
numpy.array([ ],numpy.float32):建立浮点型数组。
3、生成正交矩阵的函数ortho(l, r, b, t, n, f)
正透视
4、生成视点矩阵的函数lookAt(eye, center, up)
视点
5、生成平移矩阵的函数translate(tx, ty, tz)
生成带点(tx, ty, tz)的4×4矩阵;
6、读取链接顶点着色器和片元着色器的源程序字串的函数;
compileShader() 编译;
glCreateProgram() 创建程序;
glAttachShader() 关联
glLinkProgram()链接
7、着色器的编译函数;
glShaderSource(shader, source)替换着色器对象中的源代码
8、相机对象类。
摄像机/观察空间(Camera/View Space)
以摄像机的透视图作为场景原点时场景中所有可见顶点坐标。观察矩阵把所有的世界坐标变换到观察坐标,这些新坐标是相对于摄像机的位置和方向的。
设置了 OpenGL 的观看参数三维立体图的特征通常在于 3 个参数:眼睛位置、向上向量和方向向量。Camera类包含这些参数,并提供了一种便捷的方式,在每一个时间步骤旋转。
现在我们已经有了x轴向量和z轴向量,获取摄像机的正y轴相对简单;我们把右向量和方向向量(Direction Vector)进行叉乘。
在构造函数设置了 camera 对象的初始值。
调用 rotate()方法时,增加旋转角度,并计算旋转后新的眼睛位置和方向。
其中点(r cos(θ), r sin(θ))表示一个点,它在以原点为中心、半径为 r 的圆上,θ 是该点到原点的连线与 x 轴的夹角。转换中使用了 center,确保即使旋转中心不在原点,也能工作。
三、源代码
"""
glutils.py
Author: Mahesh Venkitachalam
Some OpenGL utilities.
"""
import OpenGL
from OpenGL.GL import *
from OpenGL.GL.shaders import *
import numpy, math
import numpy as np
from PIL import Image
def loadTexture(filename):
"""load OpenGL 2D texture from given image file"""
img = Image.open(filename)
imgData = numpy.array(list(img.getdata()), np.int8)
texture = glGenTextures(1)
glPixelStorei(GL_UNPACK_ALIGNMENT,1)
glBindTexture(GL_TEXTURE_2D, texture)
glPixelStorei(GL_UNPACK_ALIGNMENT,1)
glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE)
glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE)
glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR)
glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR)
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, img.size[0], img.size[1],
0, GL_RGBA, GL_UNSIGNED_BYTE, imgData)
glBindTexture(GL_TEXTURE_2D, 0)
return texture
def perspective(fov, aspect, zNear, zFar):
"""returns matrix equivalent for gluPerspective"""
fovR = math.radians(fov)
f = 1.0/math.tan(fovR/2.0)
return numpy.array([f/float(aspect), 0.0, 0.0, 0.0,
0.0, f, 0.0, 0.0,
0.0, 0.0, (zFar+zNear)/float(zNear-zFar), -1.0,
0.0, 0.0, 2.0*zFar*zNear/float(zNear-zFar), 0.0],
numpy.float32)
def ortho(l, r, b, t, n, f):
"""returns matrix equivalent of glOrtho"""
return numpy.array([2.0/float(r-l), 0.0, 0.0, 0.0,
0.0, 2.0/float(t-b), 0.0, 0.0,
0.0, 0.0, -2.0/float(f-n), 0.0,
-(r+l)/float(r-l), -(t+b)/float(t-b),
-(f+n)/float(f-n), 1.0],
numpy.float32)
def lookAt(eye, center, up):
"""returns matrix equivalent of gluLookAt - based on MESA implementation"""
# create an identity matrix
m = np.identity(4, np.float32)
forward = np.array(center) - np.array(eye)
norm = np.linalg.norm(forward)
forward /= norm
# normalize up vector
norm = np.linalg.norm(up)
up /= norm
# Side = forward x up
side = np.cross(forward, up)
# Recompute up as: up = side x forward
up = np.cross(side, forward)
m[0][0] = side[0]
m[1][0] = side[1]
m[2][0] = side[2]
m[0][1] = up[0]
m[1][1] = up[1]
m[2][1] = up[2]
m[0][2] = -forward[0]
m[1][2] = -forward[1]
m[2][2] = -forward[2]
# eye translation
t = np.identity(4, np.float32)
t[3][0] += -eye[0]
t[3][1] += -eye[1]
t[3][2] += -eye[2]
return t.dot(m)
def translate(tx, ty, tz):
"""creates the matrix equivalent of glTranslate"""
return np.array([1.0, 0.0, 0.0, 0.0,
0.0, 1.0, 0.0, 0.0,
0.0, 0.0, 1.0, 0.0,
tx, ty, tz, 1.0], np.float32)
def loadShaders(strVS, strFS):
"""load vertex and fragment shaders from strings"""
# compile vertex shader
shaderV = compileShader([strVS], GL_VERTEX_SHADER)
# compiler fragment shader
shaderF = compileShader([strFS], GL_FRAGMENT_SHADER)
# create the program object
program = glCreateProgram()
if not program:
raise RunTimeError('glCreateProgram faled!')
# attach shaders
glAttachShader(program, shaderV)
glAttachShader(program, shaderF)
# Link the program
glLinkProgram(program)
# Check the link status
linked = glGetProgramiv(program, GL_LINK_STATUS)
if not linked:
infoLen = glGetProgramiv(program, GL_INFO_LOG_LENGTH)
infoLog = ""
if infoLen > 1:
infoLog = glGetProgramInfoLog(program, infoLen, None);
glDeleteProgram(program)
raise RunTimeError("Error linking program:\n%s\n", infoLog);
return program
def compileShader2(source, shaderType):
"""Compile shader source of given type
source -- GLSL source-code for the shader
shaderType -- GLenum GL_VERTEX_SHADER, GL_FRAGMENT_SHADER, etc,
returns GLuint compiled shader reference
raises RuntimeError when a compilation failure occurs
"""
if isinstance(source, str):
print('string shader')
source = [source]
elif isinstance(source, bytes):
print('bytes shader')
source = [source.decode('utf-8')]
shader = glCreateShader(shaderType)
glShaderSource(shader, source)
glCompileShader(shader)
result = glGetShaderiv(shader, GL_COMPILE_STATUS)
if not(result):
# TODO: this will be wrong if the user has
# disabled traditional unpacking array support.
raise RuntimeError(
"""Shader compile failure (%s): %s"""%(
result,
glGetShaderInfoLog( shader ),
),
source,
shaderType,
)
return shader
# a simple camera class
class Camera:
"""helper class for viewing"""
def __init__(self, eye, center, up):
self.r = 10.0
self.theta = 0
self.eye = numpy.array(eye, numpy.float32)
self.center = numpy.array(center, numpy.float32)
self.up = numpy.array(up, numpy.float32)
def rotate(self):
"""rotate eye by one step"""
self.theta = (self.theta + 1) % 360
# recalculate eye
self.eye = self.center + numpy.array([
self.r*math.cos(math.radians(self.theta)),
self.r*math.sin(math.radians(self.theta)),
0.0], numpy.float32)