一、理论基础
1、三角形
使用计算机可以绘制出许多非常复杂的图形。但实际上,这些复杂图形都是由平面三角形绘制而成的。因此,三角形的绘制是本文的重点。
三角形有三个顶点(Vertex),顶点一般有以下属性:三维坐标(Coord)、颜色(Color)、法线(Normal)、纹理坐标(UV)。这些属性,一般都是在模型文件中给出的。
注意,模型文件中不一定有上面给出的所有属性,但三维坐标是必须有的。
这些是顶点的属性,但实际上,三角形中的每一个点都应该有这些属性,三角形其它点的属性应该怎么获得呢?
通过插值算法,只需要三角形顶点的属性值,就可以计算出三角形中每一个点的属性值。关于插值算法的内容,我们稍后会介绍。
等等,顶点为什么会有法线?
确实,在数学中,我们所说的法线通常是指平面的法线。但现实中可不仅仅只有平面,如果我们需要绘制一个曲面呢?曲面的每一个点的弯曲程度都不同,因此每一个点都有一个法线。但在图形学中只有一种图形——平面三角形,所以,如果我们想用平面三角形来实现曲面的效果,我们就需要为三个顶点设置不同的法线向量,而三角形中其它点的法线,则会通过插值算法计算得来。点的法线是光照理论的基础,关于光照理论的相关内容,我们会在后续章节中学习。
一个顶点可能被多个面共用,每个面都有自己的法线,那这个顶点的法线应该怎么处理?
这并不是我们需要考虑的问题。一般而言,顶点的法线会由共用它的所有面的法线加权平均得来。
纹理坐标是什么?
一个三角形不一定都是纯色的,有时候我们会用一张图来定义这个三角形中每一个点的颜色,这张图被称为纹理。纹理需要贴到三角形上,而纹理坐标就决定了纹理应该怎样贴上去。关于纹理,之后我们还会详细介绍。
2、重心坐标与插值算法
该部分内容学习与否,并不影响后续代码的编写。因此,此小节的内容供对计算机图形学的底层原理有兴趣的读者学习。
此处的插值算法,特指通过重心坐标计算三角形内的点的属性的方法。与计算机图形学中常说的另一类插值算法(最近邻插值、双线性插值等)完全无关。
对于一个三角形ABC,其内的任意一点P的坐标都可以用下面的公式表示:
{
P
=
α
A
+
β
B
+
γ
C
α
+
β
+
γ
=
1
\begin{cases} P = \alpha A+ \beta B + \gamma C \\ \\ \alpha + \beta + \gamma = 1 \end{cases}
⎩
⎨
⎧P=αA+βB+γCα+β+γ=1
(
α
,
β
,
γ
)
(\alpha, \beta, \gamma)
(α,β,γ) 即为该点的重心坐标,下面的公式给出了如何计算出
α
,
β
,
γ
\alpha, \beta, \gamma
α,β,γ:
式中的 A A , A B , A C A_A, A_B, A_C AA,AB,AC为面积,可以通过向量叉乘的方法计算出面积值。有兴趣的读者可以自行查阅资料,这里不做展开说明。
如果需要计算三角形内一点的某一属性值,则用三个顶点的属性值替换掉原式 P = α A + β B + γ C P = \alpha A+ \beta B + \gamma C P=αA+βB+γC 中的A、B、C即可求出( α , β , γ \alpha, \beta, \gamma α,β,γ由四点的坐标计算出)。
3、标准化设备坐标(NDC)
给出一个点的三维坐标后,OpenGL会通过一系列的处理,将这个三维坐标映射到 [ − 1 , 1 ] 3 [-1, 1]^3 [−1,1]3的空间内(即x、y、z三个维度的值都在-1到1之间)。这个坐标空间,被称为标准化设备坐标。
标准化设备坐标以视口的中心点为原点,向右为x轴正方向,向上为y轴正方向,x轴和 y轴的坐标范围都在-1到1之间。对于x轴而言,视口最左边为-1,最右边为1;对于y轴,视口最下面为-1,最上面为1。
二、基本图形绘制
还记得上一章的渲染循环吗?
def loop(self, render): # 注意loop函数中多了一个render参数
while not glfw.window_should_close(self.window):
glClearColor(*self.bgColor)
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT)
render() # 注意这里,相比上一章的代码,这里调用render函数,用于绘制图形
glfw.swap_buffers(self.window)
glfw.poll_events()
if glfw.get_key(self.window, glfw.KEY_ESCAPE) == glfw.PRESS:
glfw.set_window_should_close(self.window, True)
glfw.destroy_window(self.window)
glfw.terminate()
在本文接下来的内容中,如果没有特别说明,文章给出的代码,都写在render函数中。
通过glBegin
和glEnd
绘制图形
本文主要介绍通过glBegin
和glEnd
绘制图形,这种方法可以很方便的绘制基本图形。但是,当我们的模型中有大量的三角形时,这种方法就比较繁琐了。所以它并不常用。在下一章中,我们会介绍另一种更常用的绘制三角形的方法。
glBegin(GL_TRIANGLES)
# 第一个顶点
glColor(1, 0, 0)
glVertex(0, 0, 0)
# 第二个顶点
glColor(0, 1, 0)
glVertex(0.5, 0, 0)
# 第三个顶点
glColor(0, 0, 1)
glVertex(0, 0.5, 0)
glEnd()
由glBegin
定义要绘制的图形的类型。glBegin
的可使用的参数如下:
类型 | 说明 |
---|---|
GL_POINTS | 单个顶点集 |
GL_LINES | 多组双顶点线段 |
GL_POLYGON | 单个简单填充凸多边形 |
GL_TRAINGLES | 多组独立填充三角形 |
GL_QUADS | 多组独立填充四边形 |
GL_LINE_STRIP | 不闭合折线 |
GL_LINE_LOOP | 闭合折线 |
GL_TRAINGLE_STRIP | 线型连续填充三角形串 |
GL_TRAINGLE_FAN | 扇形连续填充三角形串 |
GL_QUAD_STRIP | 连续填充四边形串 |
使用glColor
设置顶点的颜色,使用glVertex
设置顶点位置(NDC坐标)。
效果图:
可以看出,三个顶点分别为红色、绿色、蓝色,三角形中的其它点的颜色,则通过插值算法获得。
如果我们稍微调整一下三个顶点的顺序:
glBegin(GL_TRIANGLES)
# 第一个顶点
glColor(0, 0, 1)
glVertex(0, 0.5, 0)
# 第二个顶点
glColor(0, 1, 0)
glVertex(0.5, 0, 0)
# 第三个顶点
glColor(1, 0, 0)
glVertex(0, 0, 0)
glEnd()
此时再运行代码,就会发现三角形不见了!
这是背面剔除产生的效果。还记得上一章中有这样一句代码吗?
glEnable(GL_CULL_FACE)
这句代码的功能就是开启背面剔除。何为背面剔除?对于一个在空间中的三角形,有时候我们只会看到它的一个面(正面),而另一个面(背面)不可见。此时,我们可以不绘制三角形的背面以节省资源,这就是背面剔除。
那么怎么区分三角形的正面和背面呢?OpenGL采用右手法则。右手四指(除大拇指)按照顶点的书写顺序握拳,大拇指朝外。此时大拇指的方向即为三角形的正面。
如果你学习过向量叉乘,那么如果一个三角形按照ABC的顺序书写,AB × AC的方向即为正面。
如下图,三角形OAB(O为原点)的正面为OC方向。而三角形OBA的正面则为OC的反方向。
由于glBegin
使用较少,它的用法就介绍到这里,更详细的内容,请参阅OpenGL官方文档。
三、微调Window类
在上一篇文章给出的Window
类中,我们将所有物体的渲染代码都写在loop()
方法里。但在实际开发过程中,Window
作为一个工具类,其内部代码不应被修改。因此,我们需要为使用Window
类的用户提供一个接口,以便于其将自己的渲染代码插入到渲染循环中。
具体来说,我们为Window类添加一个self.render_events
列表,并添加一个self.add_render_event()
方法,让用户将自己的渲染函数注册到Window类中,在oop()
方法内,我们在每一帧都调用self.render_events
列表内的所有函数。具体代码如下:
class Window:
def __init__(self, width, height, title, bgColor=(0.0, 0.0, 0.0, 1.0)):
# 此处代码与之前相同,故省略
self.render_event = []
# 用于注册渲染事件的方法
def add_render_event(self, render_event):
self.render_event.append(render_event)
# 渲染循环
def loop(self):
while not glfw.window_should_close(self.window):
glClearColor(*self.bgColor)
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT)
# 此处调用渲染事件,绘制物体
for render_event in self.render_event:
render_event()
glfw.swap_buffers(self.window)
glfw.poll_events()
if glfw.get_key(self.window, glfw.KEY_ESCAPE) == glfw.PRESS:
glfw.set_window_should_close(self.window, True)
glfw.destroy_window(self.window)
glfw.terminate()
封装好Window
类后,使用如下代码即可绘制出三角形。
def render():
glBegin(GL_TRIANGLES)
# 第一个顶点
glColor(1, 0, 0)
glVertex(0, 0, 0)
# 第二个顶点
glColor(0, 1, 0)
glVertex(0.5, 0, 0)
# 第三个顶点
glColor(0, 0, 1)
glVertex(0, 0.5, 0)
glEnd()
if __name__ == '__main__':
w = Window(1920, 1080, "Test")
w.add_render_event(render)
w.loop()
四、结语
本文主要介绍了NDC坐标、重心坐标、插值算法,以及如何使用PyOpenGL绘制三角形。在下一章中,我们将介绍另一种更常用的绘制三角形的方法,同时简要介绍渲染管线的相应内容。