上图左是平行投影模式的显示效果,上图右是透视投影模式的显示效果。代码如下:
# -*- coding: utf-8 -*-
# -------------------------------------------
# quidam_02.py 旋转、缩放、改变视点和参考点
# -------------------------------------------
from OpenGL.GL import *
from OpenGL.GLU import *
from OpenGL.GLUT import *
import numpy as np
IS_PERSPECTIVE = True # 透视投影
VIEW = np.array([-0.8, 0.8, -0.8, 0.8, 1.0, 20.0]) # 视景体的left/right/bottom/top/near/far六个面
SCALE_K = np.array([1.0, 1.0, 1.0]) # 模型缩放比例
EYE = np.array([0.0, 0.0, 2.0]) # 眼睛的位置(默认z轴的正方向)
LOOK_AT = np.array([0.0, 0.0, 0.0]) # 瞄准方向的参考点(默认在坐标原点)
EYE_UP = np.array([0.0, 1.0, 0.0]) # 定义对观察者而言的上方(默认y轴的正方向)
WIN_W, WIN_H = 640, 480 # 保存窗口宽度和高度的变量
LEFT_IS_DOWNED = False # 鼠标左键被按下
MOUSE_X, MOUSE_Y = 0, 0 # 考察鼠标位移量时保存的起始位置
def getposture:
global EYE, LOOK_AT
dist = np.sqrt(np.power((EYE-LOOK_AT), 2).sum)
if dist > 0:
phi = np.arcsin((EYE[1]-LOOK_AT[1])/dist)
theta = np.arcsin((EYE[0]-LOOK_AT[0])/(dist*np.cos(phi)))
else:
phi = 0.0
theta = 0.0
return dist, phi, theta
DIST, PHI, THETA = getposture # 眼睛与观察目标之间的距离、仰角、方位角
def init:
glClearColor(0.0, 0.0, 0.0, 1.0) # 设置画布背景色。注意:这里必须是4个参数
glEnable(GL_DEPTH_TEST) # 开启深度测试,实现遮挡关系
glDepthFunc(GL_LEQUAL) # 设置深度测试函数(GL_LEQUAL只是选项之一)
def draw:
global IS_PERSPECTIVE, VIEW
global EYE, LOOK_AT, EYE_UP
global SCALE_K
global WIN_W, WIN_H
# 清除屏幕及深度缓存
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT)
# 设置投影(透视投影)
glMatrixMode(GL_PROJECTION)
glLoadIdentity
if WIN_W > WIN_H:
if IS_PERSPECTIVE:
glFrustum(VIEW[0]*WIN_W/WIN_H, VIEW[1]*WIN_W/WIN_H, VIEW[2], VIEW[3], VIEW[4], VIEW[5])
else:
glOrtho(VIEW[0]*WIN_W/WIN_H, VIEW[1]*WIN_W/WIN_H, VIEW[2], VIEW[3], VIEW[4], VIEW[5])
else:
if IS_PERSPECTIVE:
glFrustum(VIEW[0], VIEW[1], VIEW[2]*WIN_H/WIN_W, VIEW[3]*WIN_H/WIN_W, VIEW[4], VIEW[5])
else:
glOrtho(VIEW[0], VIEW[1], VIEW[2]*WIN_H/WIN_W, VIEW[3]*WIN_H/WIN_W, VIEW[4], VIEW[5])
# 设置模型视图
glMatrixMode(GL_MODELVIEW)
glLoadIdentity
# 几何变换
glScale(SCALE_K[0], SCALE_K[1], SCALE_K[2])
# 设置视点
gluLookAt(
EYE[0], EYE[1], EYE[2],
LOOK_AT[0], LOOK_AT[1], LOOK_AT[2],
EYE_UP[0], EYE_UP[1], EYE_UP[2]
)
# 设置视口
glViewport(0, 0, WIN_W, WIN_H)
# ---------------------------------------------------------------
glBegin(GL_LINES) # 开始绘制线段(世界坐标系)
# 以红色绘制x轴
glColor4f(1.0, 0.0, 0.0, 1.0) # 设置当前颜色为红色不透明
glVertex3f(-0.8, 0.0, 0.0) # 设置x轴顶点(x轴负方向)
glVertex3f(0.8, 0.0, 0.0) # 设置x轴顶点(x轴正方向)
# 以绿色绘制y轴
glColor4f(0.0, 1.0, 0.0, 1.0) # 设置当前颜色为绿色不透明
glVertex3f(0.0, -0.8, 0.0) # 设置y轴顶点(y轴负方向)
glVertex3f(0.0, 0.8, 0.0) # 设置y轴顶点(y轴正方向)
# 以蓝色绘制z轴
glColor4f(0.0, 0.0, 1.0, 1.0) # 设置当前颜色为蓝色不透明
glVertex3f(0.0, 0.0, -0.8) # 设置z轴顶点(z轴负方向)
glVertex3f(0.0, 0.0, 0.8) # 设置z轴顶点(z轴正方向)
glEnd # 结束绘制线段
# ---------------------------------------------------------------
glBegin(GL_TRIANGLES) # 开始绘制三角形(z轴负半区)
glColor4f(1.0, 0.0, 0.0, 1.0) # 设置当前颜色为红色不透明
glVertex3f(-0.5, -0.366, -0.5) # 设置三角形顶点
glColor4f(0.0, 1.0, 0.0, 1.0) # 设置当前颜色为绿色不透明
glVertex3f(0.5, -0.366, -0.5) # 设置三角形顶点
glColor4f(0.0, 0.0, 1.0, 1.0) # 设置当前颜色为蓝色不透明
glVertex3f(0.0, 0.5, -0.5) # 设置三角形顶点
glEnd # 结束绘制三角形
# ---------------------------------------------------------------
glBegin(GL_TRIANGLES) # 开始绘制三角形(z轴正半区)
glColor4f(1.0, 0.0, 0.0, 1.0) # 设置当前颜色为红色不透明
glVertex3f(-0.5, 0.5, 0.5) # 设置三角形顶点
glColor4f(0.0, 1.0, 0.0, 1.0) # 设置当前颜色为绿色不透明
glVertex3f(0.5, 0.5, 0.5) # 设置三角形顶点
glColor4f(0.0, 0.0, 1.0, 1.0) # 设置当前颜色为蓝色不透明
glVertex3f(0.0, -0.366, 0.5) # 设置三角形顶点
glEnd # 结束绘制三角形
# ---------------------------------------------------------------
glutSwapBuffers # 切换缓冲区,以显示绘制内容
def reshape(width, height):
global WIN_W, WIN_H
WIN_W, WIN_H = width, height
glutPostRedisplay
def mouseclick(button, state, x, y):
global SCALE_K
global LEFT_IS_DOWNED
global MOUSE_X, MOUSE_Y
MOUSE_X, MOUSE_Y = x, y
if button == GLUT_LEFT_BUTTON:
LEFT_IS_DOWNED = state==GLUT_DOWN
elif button == 3:
SCALE_K *= 1.05
glutPostRedisplay
elif button == 4:
SCALE_K *= 0.95
glutPostRedisplay
def mousemotion(x, y):
global LEFT_IS_DOWNED
global EYE, EYE_UP
global MOUSE_X, MOUSE_Y
global DIST, PHI, THETA
global WIN_W, WIN_H
if LEFT_IS_DOWNED:
dx = MOUSE_X - x
dy = y - MOUSE_Y
MOUSE_X, MOUSE_Y = x, y
PHI = 2*np.pi*dy/WIN_H
PHI %= 2*np.pi
THETA = 2*np.pi*dx/WIN_W
THETA %= 2*np.pi
r = DIST*np.cos(PHI)
EYE[1] = DIST*np.sin(PHI)
EYE[0] = r*np.sin(THETA)
EYE[2] = r*np.cos(THETA)
if 0.5*np.pi < PHI < 1.5*np.pi:
EYE_UP[1] = -1.0
else:
EYE_UP[1] = 1.0
glutPostRedisplay
def keydown(key, x, y):
global DIST, PHI, THETA
global EYE, LOOK_AT, EYE_UP
global IS_PERSPECTIVE, VIEW
if key in [b'x', b'X', b'y', b'Y', b'z', b'Z']:
if key == b'x': # 瞄准参考点 x 减小
LOOK_AT[0] -= 0.01
elif key == b'X': # 瞄准参考 x 增大
LOOK_AT[0] = 0.01
elif key == b'y': # 瞄准参考点 y 减小
LOOK_AT[1] -= 0.01
elif key == b'Y': # 瞄准参考点 y 增大
LOOK_AT[1] = 0.01
elif key == b'z': # 瞄准参考点 z 减小
LOOK_AT[2] -= 0.01
elif key == b'Z': # 瞄准参考点 z 增大
LOOK_AT[2] = 0.01
DIST, PHI, THETA = getposture
glutPostRedisplay
elif key == b'\r': # 回车键,视点前进
EYE = LOOK_AT (EYE - LOOK_AT) * 0.9
DIST, PHI, THETA = getposture
glutPostRedisplay
elif key == b'\x08': # 退格键,视点后退
EYE = LOOK_AT (EYE - LOOK_AT) * 1.1
DIST, PHI, THETA = getposture
glutPostRedisplay
elif key == b' ': # 空格键,切换投影模式
IS_PERSPECTIVE = not IS_PERSPECTIVE
glutPostRedisplay
if __name__ == "__main__":
glutInit
displayMode = GLUT_DOUBLE | GLUT_ALPHA | GLUT_DEPTH
glutInitDisplayMode(displayMode)
glutInitWindowSize(WIN_W, WIN_H)
glutInitWindowPosition(300, 200)
glutCreateWindow('Quidam Of OpenGL')
init # 初始化画布
glutDisplayFunc(draw) # 注册回调函数draw
glutReshapeFunc(reshape) # 注册响应窗口改变的函数reshape
glutMouseFunc(mouseclick) # 注册响应鼠标点击的函数mouseclick
glutMotionFunc(mousemotion) # 注册响应鼠标拖拽的函数mousemotion
glutKeyboardFunc(keydown) # 注册键盘输入的函数keydown
glutMainLoop # 进入glut主循环
十二、小结
虽然还有很多领域需要我们继续探索,比如灯光、材质、雾化、拾取等,但那不是奇幻之旅的目标。奇幻之旅仅仅是帮助读者建立 OpenGL 的基本概念。至此,我们基本完成了任务。
加速渲染
实际应用 OpenGL 绘制三维图像时,往往需要处理数以万计的顶点,有时甚至是百万级、千万级。我们通常不会在绘制函数里面传送这些数据,而是在绘制之前,将这些数据提前传送到GPU。绘制函数每次绘制时,只需要从GPU的缓存中取出数据即可,极大地提高了效率。这个机制地实现,依赖于顶点缓冲区对象(Vertex Buffer Object),简称VBO。
尽管 VBO 是显卡的扩展,其实没有用到GPU运算,也就是说 VBO 不用写着色语言,直接用opengl函数就可以调用,主要目的是用于加快渲染的速。
VBO 将顶点信息放到 GPU 中,GPU 在渲染时去缓存中取数据,二者中间的桥梁是 GL-Context。GL-Context 整个程序一般只有一个,所以如果一个渲染流程里有两份不同的绘制代码,GL-context 就负责在他们之间进行切换。这也是为什么要在渲染过程中,在每份绘制代码之中会有 glBindbuffer、glEnableVertexAttribArray、glVertexAttribPointer。如果把这些都放到初始化时候完成,使用一种结构记录该次绘制所需要的所有 VBO 所需信息,把它保存到 VBO特定位置,绘制的时候直接在这个位置取信息绘制,会简化渲染流程、提升渲染速度。这就是 VAO 概念产生的初衷。
VAO 的全名是 Vertex Array Object,首先,它不是 Buffer-Object,所以不用作存储数据;其次,它针对“顶点”而言,也就是说它跟“顶点的绘制”息息相关。VAO 记录的是一次绘制中所需要的信息,这包括“数据在哪里 glBindBuffer”、“数据的格式是怎么样的 glVertexAttribPointer”、shader-attribute 的 location 的启用 glEnableVertexAttribArray。
根据我查到的资料,几乎所有的显卡都支持 VBO,但不是所有的显卡都支持 VAO,而 VAO 仅仅是优化了 VBO 的使用方法,对于加速并没有实质性的影响,因此本文只讨论 VBO 技术。
一、创建顶点缓冲区对象(VBO)
假定画一个六面体,顶点是这样的:
# -*- coding: utf-8 -*-
# 六面体数据
# ------------------------------------------------------
# v4----- v5
# /| /|
# v0------v1|
# | | | |
# | v7----|-v6
# |/ |/
# v3------v2
# 顶点集
vertices = np.array([
-0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, -0.5, 0.5, -0.5, -0.5, 0.5, # v0-v1-v2-v3
-0.5, 0.5, -0.5, 0.5, 0.5, -0.5, 0.5, -0.5, -0.5, -0.5, -0.5, -0.5 # v4-v5-v6-v7
], dtype=np.float32)
# 索引集
indices = np.array([
0, 1, 2, 3, # v0-v1-v2-v3 (front)
4, 5, 1, 0, # v4-v5-v1-v0 (top)
3, 2, 6, 7, # v3-v2-v6-v7 (bottom)
5, 4, 7, 6, # v5-v4-v7-v6 (back)
1, 5, 6, 2, # v1-v5-v6-v2 (right)
4, 0, 3, 7 # v4-v0-v3-v7 (left)
], dtype=np.int)
在GPU上创建VBO如下:
from OpenGL.arrays import vbo
vbo_vertices = vbo.VBO(vertices)
vbo_indices = vbo.VBO(indices, target=GL_ELEMENT_ARRAY_BUFFER)
创建 顶点 VBO 时,默认 target=GL_ARRAY_BUFFER, 而创建索引 VBO 时,target=GL_ELEMENT_ARRAY_BUFFER,因为顶点的数据类型是 np.float32,索引的数据类型是np.int。
在VBO保存的顶点数据集,除了顶点信息外,还可以包含颜色、法线、纹理等数据,这就是顶点混合数组的概念。假定我们在上面的顶点集中增加每个顶点的颜色,则可以写成这样:
vertices = np.array([
0.3, 0.6, 0.9, -0.35, 0.35, 0.35, # c0-v0
0.6, 0.9, 0.3, 0.35, 0.35, 0.35, # c1-v1
0.9, 0.3, 0.6, 0.35, -0.35, 0.35, # c2-v2
0.3, 0.9, 0.6, -0.35, -0.35, 0.35, # c3-v3
0.6, 0.3, 0.9, -0.35, 0.35, -0.35, # c4-v4
0.9, 0.6, 0.3, 0.35, 0.35, -0.35, # c5-v5
0.3, 0.9, 0.9, 0.35, -0.35, -0.35, # c6-v6
0.9, 0.9, 0.3, -0.35, -0.35, -0.35 # c7-v7
], dtype=np.float32)
二、分离顶点混合数组
使用 glInterleavedArrays 函数可以从顶点混合数组中分离顶点、颜色、法线和纹理。比如,对只包含顶点信息的顶点混合数组:
vbo_indices.bind
glInterleavedArrays(GL_V3F, 0, None)
如果顶点混合数组包含了颜色和顶点信息:
vbo_indices.bind
glInterleavedArrays(GL_C3F_V3F, 0, None)
glInterleavedArrays 函数第一个参数总共有14个选项,分别是:
GL_V2F
GL_V3F
GL_C4UB_V2F
GL_C4UB_V3F
GL_C3F_V3F
GL_N3F_V3F
GL_C4F_N3F_V3F
GL_T2F_V3F
GL_T4F_V4F
GL_T2F_C4UB_V3F
GL_T2F_C3F_V3F
GL_T2F_N3F_V3F
GL_T2F_C4F_N3F_V3F
GL_T4F_C4F_N3F_V4F
三、使用顶点缓冲区对象(VBO)
使用glDrawElements 等函数绘制前,需要先绑定顶点数据集和索引数据集,然后使用glInterleavedArrays 分理出顶点、颜色、法线等数据。
vbo_indices.bind
glInterleavedArrays(GL_V3F, 0, None)
vbo_indices.bind
glDrawElements(GL_QUADS, int(vbo_indices .size/4), GL_UNSIGNED_INT, None)
vbo_indices.unbind
vbo_indices.unbind
致谢:
写作过程中,我参考了很多资料,包括纸质书籍和网页,列写于此,一并致谢!
《OpenGL编程精粹》杨柏林 陈根浪 徐静 编著
Opengl开发库介绍
https://blog.csdn.net/yyyuhan/article/details/2045009
OpenGL的API函数使用手册
https://www.cnblogs.com/1024Planet/p/5764646.html
glut处理鼠标事件
https://blog.csdn.net/jacky_chenjp/article/details/69396540/
Learn OpenGL
https://learnopengl-cn.github.io/
原文:https://blog.csdn.net/xufive/article/details/86565130
声明:本文系CSDN博客原创文章,转载请联系原作者。
技术的道路一个人走着极为艰难?
一身的本领得不施展?
优质的文章得不到曝光?