PyOpenGL代码实战(二):基本图形绘制——glBegin

本文介绍OpenGL中的三角形绘制原理及实现方法,包括重心坐标、插值算法等,并演示如何使用PyOpenGL绘制带颜色插值的三角形。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

一、理论基础

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函数中。

通过glBeginglEnd绘制图形

本文主要介绍通过glBeginglEnd绘制图形,这种方法可以很方便的绘制基本图形。但是,当我们的模型中有大量的三角形时,这种方法就比较繁琐了。所以它并不常用。在下一章中,我们会介绍另一种更常用的绘制三角形的方法。

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绘制三角形。在下一章中,我们将介绍另一种更常用的绘制三角形的方法,同时简要介绍渲染管线的相应内容。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值