PyOpenGL代码实战(五):纹理

一、理论基础

纹理与纹理坐标

在前面的学习中,我们已经成功在窗口中绘制出了三角形,并且我们通过顶点数据为每一个顶点设置了颜色,而三角形内的点的颜色则有硬件通过插值计算得来。但是,在更多时候,我们不会使用顶点的颜色属性,会用一张图片直接定义三角形中每一个点的颜色,这张图片就被称之为纹理

你可以将纹理看作是一张贴在三角形上的图片。而图片贴在三角形上的方式是多样的,可以正着贴,也可以倒着贴,当然也可以斜着贴。为每一个顶点设置纹理坐标(UV或TexCoord)即可固定纹理贴在三角形上的方式。

二、具体用法

1、顶点数据

我们稍微修改上一章提供的代码。

首先,我们提供的顶点属性如下:

triangle = np.array([
    # 顶点NDC坐标		 纹理坐标
    -0.5, -0.5, 0, 		  0, 0,
     0.5, -0.5, 0, 		  1, 0,
       0,  0.5, 0, 		0.5, 1
], dtype=np.float32)

可以看到,我们不再提供顶点的颜色属性,转而提供顶点的纹理坐标属性。纹理坐标是一个vec2类型的变量,故只需要两个浮点数即可。

同时,由于顶点数据发生了改变,我们原来的解释顶点数据的语句也需要发生改变:

shader.setAttrib("aPos", 3, GL_FLOAT, 20, 0)
shader.setAttrib("aTex", 2, GL_FLOAT, 20, 12)

我们不再设置aColor变量,而设置顶点的aTex变量,它是一个vec2类型的变量,故其size为2。注意,此时顶点属性的步长有原来的24字节变成了20字节(相比于原来少了一个浮点数)。

以下为完整的Python代码:

import numpy as np
from OpenGL.GL import *
from OpenGL.arrays.vbo import VBO

# 导入Window类和Shader类
from shader import Shader
from window import Window

w = Window(1920, 1080, "Test")

triangle = np.array([
    -0.5, -0.5, 0, 0, 0,
    0.5, -0.5, 0, 1, 0,
    0, 0.5, 0, 0, 0.5, 1
], dtype=np.float32)

vao = glGenVertexArrays(1)
glBindVertexArray(vao)

vbo = VBO(triangle, GL_STATIC_DRAW)
vbo.bind()

# 导入顶点着色器文件和片元着色器文件
shader = Shader("base.vert", "base.frag")
shader.setAttrib("aPos", 3, GL_FLOAT, 20, 0)
shader.setAttrib("aTex", 2, GL_FLOAT, 20, 12)

def render():
    shader.use()
    glBindVertexArray(vao)
    glDrawArrays(GL_TRIANGLES, 0, 3)

w.loop(render)

同时,着色器程序也要发生改变:

顶点着色器:

#version 330 core
in vec3 aPos;
in vec2 aTex;	// 顶点的纹理坐标属性
out vec2 uv;	// 将纹理坐标传给片元着色器

void main()
{
    gl_Position = vec4(aPos, 1.0f);
    uv = aTex;	// 将纹理坐标传给片元着色器
}

由于纹理坐标是针对三角形内每一个像素的,所以我们要把纹理坐标传给片元着色器。

片元着色器:

#version 330 core
in vec2 uv;	// 纹理坐标
out vec4 FragColor;
uniform sampler2D tex;	// 纹理

void main()
{
    FragColor = texture(tex, uv);	// 从纹理中采样
}

我们接收了从顶点着色器传来的纹理坐标in vec2 uv。我们定义了一个表示纹理的变量tex,它的变量类型为sampler2D,注意这个变量是一个uniform变量,因为我们之后要通过Python代码把纹理数据传给片元着色器。

在主函数中,调用了texture(tex, uv)函数,这个函数的含义是通过纹理坐标在纹理的相应位置进行采样,获得一个vec4的颜色值,并将颜色值赋给这个像素以显示纹理。

这时候如果我们运行代码,只能看到一片漆黑,因为我们还没有把纹理数据传给着色器,texture函数无法获取到正确的值。

2、将纹理数据传给GPU

本文使用的纹理图:
在这里插入图片描述

图源自LearnOpenGL CN

接下来介绍在PyOpenGL中,如何将纹理数据传给GPU

首先,我们通过Python的PIL库来读取图片:

from PIL import Image

img = Image.open("wall.jpg") # 读取文件
img = np.array(img, np.int8) # 将Image转为np.int8类型的numpy数组(PyOpenGL规定必须要这种类型)
texHeight, texWidth, _ = img.shape	# 获取图片的宽高

在OpenGL中,纹理也是一个对象。通过glGenTextures(1)函数可以创建纹理对象,通过glBindTexture(GL_TEXTURE_2D, tex)来绑定纹理对象:

tex = glGenTextures(1)
glBindTexture(GL_TEXTURE_2D, tex)
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, texWidth, texHeight, 0, GL_RGB, GL_UNSIGNED_BYTE, img)
glGenerateMipmap(GL_TEXTURE_2D)

glTexImage2D,这个函数用于将纹理数据传递给GPU。其参数较多,我们注意介绍:

第一个参数:纹理类型。纹理类型主要有两种。

纹理类型含义
GL_TEXTURE_2D2D纹理
GL_TEXTURE_CUBE_MAP立方体纹理

立方体纹理将在以后介绍。

第二个参数:指定多级渐远纹理(Mipmap)的级别。关于Mipmap,我们稍后介绍。现在只需要知道,这个参数一般填0即可。

第三个参数:把纹理存储为何种形式。主要有GL_RGBGL_RGBA两种。

第四、第五个参数:纹理的宽高。

第六个参数:由于历史遗留问题,这个参数总是填0。

第七、第八个参数:纹理的格式和数据类型。

第九个参数:纹理数据。

这里有一个表,解释了内部格式(参数三)、图片格式(参数七)、数据类型(参数八)之间的关系:

内部格式图片格式数据类型
GL_RGBGL_RGBGL_UNSIGNED_BYTE
GL_RGBAGL_RGBAGL_UNSIGNED_BYTE
GL_ALPHAGL_ALPHAGL_UNSIGNED_BYTE

上表只是列出了常用的几种格式,并非 OpenGL 支持的所有格式。

从上表可以看出,参数三和参数七一般相同,参数八一般为GL_UNSIGNED_BYTE

glGenerateMipmap:生成多级渐远纹理(Mipmap)。这里有一篇微软对Mipmap的介绍。简而言之,Mipmap就是对于同一张图像,当它的尺寸发生变化时,我们用不同像素的纹理来显示这个图像,这种方法可以显著的改善因图片缩小带来的失真问题,但会占用更多的内存。通过调用glGenerateMipmap,OpenGL会自动为我们生成不同像素的纹理序列。

3、将纹理索引传给着色器

glActiveTexture(GL_TEXTURE0)
shader.setUniform("tex", 0)

对于每一个纹理,我们都需要为它分配一个索引值,以便着色器能通过索引值获取到纹理数据;另外,当同一个着色器需要多张纹理时,也需要用索引值来区分不同的纹理。glActiveTexture(GL_TEXTURE0)为当前绑定的纹理分配了索引值0,该函数的参数为GL_TEXTURE{X},X可以是0~31。再通过设置uniform变量的方法,将该索引值传递给着色器。

4、纹理相关设置

glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT)
glTexParameteri(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_MIPMAP_LINEAR)

通过glTexParameteri,可以设置当前绑定的纹理的一些属性。该函数有三个参数,第一个参数为纹理类型,第二个参数为要修改的纹理的属性,第三个参数为该属性的值。

属性含义
GL_TEXTURE_WRAP_S纹理X轴的环绕方式
GL_TEXTURE_WRAP_T纹理Y轴的环绕方式
GL_TEXTURE_MAG_FILTER纹理放大时的插值算法
GL_TEXTURE_MIN_FILTER纹理缩小时的插值算法

上表中有两个名词需要解释:

首先,环绕方式,是指当纹理坐标在[0, 1]之外时的处理方式。

环绕方式描述
GL_REPEAT环绕方式的默认值。重复纹理图像。
GL_MIRRORED_REPEAT和GL_REPEAT一样,但每次重复图片是镜像放置的。
GL_CLAMP_TO_EDGE超出的部分会重复纹理坐标的边缘,产生一种边缘被拉伸的效果。
GL_CLAMP_TO_BORDER超出的坐标为用户指定的边缘颜色。

在这里插入图片描述

其次,插值方式,即当图片放大缩小时,如何处理纹理。

插值方式描述
GL_NEAREST最近邻插值
GL_LINEAR双线性插值
GL_LINEAR_MIPMAP_LINEAR三线性插值

这三种插值算法的具体含义这里不做介绍,有兴趣的读者可以自行查阅资料。注意查阅资料时区分双三次插值和三线性插值。

5、结果

至此,我们得到了绘制纹理的完整代码:

import numpy as np
from OpenGL.GL import *
from OpenGL.arrays.vbo import VBO
from PIL import Image
from shader import Shader
from window import Window
# 创建窗口
w = Window(1920, 1080, "Test")
# 顶点数据
triangle = np.array([
    -0.5, -0.5, 0, 0, 0,
    0.5, -0.5, 0, 1, 0,
    0, 0.5, 0, 0, 0.5, 1
], dtype=np.float32)
# 创建VAO
vao = glGenVertexArrays(1)
glBindVertexArray(vao)
# 创建VBO
vbo = VBO(triangle, GL_STATIC_DRAW)
vbo.bind()
# 导入顶点着色器文件和片元着色器文件
shader = Shader("base.vert", "base.frag")
# 设置顶点属性
shader.setAttrib("aPos", 3, GL_FLOAT, 20, 0)
shader.setAttrib("aTex", 2, GL_FLOAT, 20, 12)
# 读取纹理数据
img = Image.open("wall.jpg")
img = np.array(img, np.int8)
texWidth, texHeight, _ = img.shape
# 创建纹理对象
tex = glGenTextures(1)
glBindTexture(GL_TEXTURE_2D, tex)
# 将纹理数据传给GPU
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, texWidth, texHeight, 0, GL_RGB, GL_UNSIGNED_BYTE, img)
glGenerateMipmap(GL_TEXTURE_2D)
# 将纹理索引传给着色器
glActiveTexture(GL_TEXTURE0)
shader.setUniform("tex", 0)
# 配置
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT)
glTexParameteri(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_MIPMAP_LINEAR)
# 渲染循环
def render():
    shader.use()
    glBindVertexArray(vao)
    glDrawArrays(GL_TRIANGLES, 0, 3)
w.loop(render)

着色器代码在上面已经给出。

最终结果图:
在这里插入图片描述

三、封装

下面我们将与纹理有关的操作封装成Texture类,以提高程序的复用性和可扩展性。

from OpenGL.GL import *
from PIL import Image
import numpy as np


class Texture:
    def __init__(self, imgPath, idx=0, texType=GL_TEXTURE_2D,
                 imgType=GL_RGB, innerType=None, dataType=GL_UNSIGNED_BYTE):
        if not innerType:
            innerType = imgType
        self.idx = idx
        # 创建纹理对象
        self.tex = glGenTextures(1)
        glBindTexture(GL_TEXTURE_2D, self.tex)
        # 读取纹理数据
        img = Image.open(imgPath)
        img = np.array(img, np.int8)
        self.h, self.w, _ = img.shape
        # 将纹理数据传给GPU
        glTexImage2D(texType, 0, innerType, self.h, self.w, 0, imgType, dataType, img)
        glGenerateMipmap(texType)
        # 纹理设置
        glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT)
        glTexParameteri(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_MIPMAP_LINEAR)

    def setIndex(self, shader, name=None):
        """ 设置纹理索引 """
        if not name:
            name = "tex" + str(self.idx)
        glBindTexture(GL_TEXTURE_2D, self.tex)
        glActiveTexture(GL_TEXTURE0 + self.idx)
        shader.setUniform(name, self.idx)

封装后,主程序代码:

import numpy as np
from OpenGL.GL import *
from OpenGL.arrays.vbo import VBO
from shader import Shader
from texture import Texture
from window import Window

# 创建窗口
w = Window(1920, 1080, "Test")
# 顶点数据
triangle = np.array([
    -0.5, -0.5, 0, 0, 0,
    0.5, -0.5, 0, 1, 0,
    0, 0.5, 0, 0.5, 1
], dtype=np.float32)
# VAO
vao = glGenVertexArrays(1)
glBindVertexArray(vao)
# VBO
vbo = VBO(triangle, GL_STATIC_DRAW)
vbo.bind()
# 导入顶点着色器文件和片元着色器文件
shader = Shader("base.vert", "base.frag")
shader.setAttrib("aPos", 3, GL_FLOAT, 20, 0)
shader.setAttrib("aTex", 2, GL_FLOAT, 20, 12)
# 创建并设置纹理
tex = Texture("wall.jpg")
tex.setIndex(shader, "tex")


# 渲染循环
def render():
    shader.use()
    glBindVertexArray(vao)
    glDrawArrays(GL_TRIANGLES, 0, 3)


w.loop(render)

四、结语

本文介绍了在OpenGL中如何使用纹理。在这次以及之前的教程中,我们给出的坐标都是NDC坐标,在下一节中,我们将介绍将模型坐标转换为NDC坐标的方法。

  • 2
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 5
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 5
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值