【OpenGL】笔记五、纹理

2023-11-03

1. 流程

1.1 单个纹理

纹理是一个2D图片(甚至也有1D和3D的纹理),它可以用来添加物体的细节,为了能够使用纹理图片,我们需要一个叫做stb_image.h的头文件库来加载不同格式的图片作为纹理(全部文件),得到该头文件后,加入项目,并且新建一个叫做stb_image_wrap.cpp的文件,写入:

#define STB_IMAGE_IMPLEMENTATION
#include "stb_image.h"

然后在主程序里加入头文件:

#include "stb_image.h"

这样就可以加载自定义纹理图了(加载前使用stbi_set_flip_vertically_on_load(true);保证读取图片不会上下颠倒)

int width, height, nrChannels;
unsigned char *data = stbi_load("container.jpg", &width, &height, &nrChannels, 0);

这里我们使用stbi_load读取图片数据,顺便获取宽高和颜色通道数等信息,接下来要做的其实就是新建绑定纹理了,这和之前的VBO很相似:

unsigned int texture;
glGenTextures(1, &texture);
glBindTexture(GL_TEXTURE_2D, texture);

做完这些,我们还有一些纹理的性质需要定义:

// 为当前绑定的纹理对象设置环绕、过滤方式
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_MIN_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);

头两行是设定纹理坐标超过范围时该怎么着色:
在这里插入图片描述
后两行是设定在放大和缩小图片时我们应该对纹理进行什么形式的插值:
在这里插入图片描述
设定完性质以后,我们对之前加载的图片数据进行检测,如果有效,则利用glTexImage2D生成纹理(

第一个参数指定了纹理目标(Target)。设置为GL_TEXTURE_2D意味着会生成与当前绑定的纹理对象在同一个目标上的纹理(任何绑定到GL_TEXTURE_1D和GL_TEXTURE_3D的纹理不会受到影响)。
第二个参数为纹理指定多级渐远纹理的级别,如果你希望单独手动设置每个多级渐远纹理的级别的话。这里我们填0,也就是基本级别。
第三个参数告诉OpenGL我们希望把纹理储存为何种格式。我们的图像只有RGB值,因此我们也把纹理储存为RGB值。
第四个和第五个参数设置最终的纹理的宽度和高度。我们之前加载图像的时候储存了它们,所以我们使用对应的变量。
下个参数应该总是被设为0(历史遗留的问题)。
第七第八个参数定义了源图的格式和数据类型。我们使用RGB值加载这个图像,并把它们储存为char(byte)数组,我们将会传入对应值。
最后一个参数是真正的图像数据。

)和利用glGenerateMipmap生成mipmap(多级渐远纹理),最后释放数据:

int width, height, nrChannels;
unsigned char *data = stbi_load("container.jpg", &width, &height, &nrChannels, 0);
if (data)
{
    glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, width, height, 0, GL_RGB, GL_UNSIGNED_BYTE, data);
    glGenerateMipmap(GL_TEXTURE_2D);
}
else
{
    std::cout << "Failed to load texture" << std::endl;
}
stbi_image_free(data);

那么纹理加载好了,我们该怎么确定哪部分的纹理加载到图形的哪部分呢?

为了能够把纹理映射(Map)到三角形上,我们需要指定三角形的每个顶点各自对应纹理的哪个部分。这样每个顶点就会关联着一个纹理坐标

纹理坐标在x和y轴上,范围为0到1之间(注意我们使用的是2D纹理图像)。使用纹理坐标获取纹理颜色叫做采样(Sampling)。纹理坐标起始于(0, 0),也就是纹理图片的左下角,终始于(1, 1),即纹理图片的右上角,如下:
在这里插入图片描述
我们一样可以将纹理的映射写进着色器:

#version 330 core
layout (location = 0) in vec3 aPos;
layout (location = 1) in vec3 aColor;
layout (location = 2) in vec2 aTexCoord;

out vec3 ourColor;
out vec2 TexCoord;

void main()
{
    gl_Position = vec4(aPos, 1.0);
    ourColor = aColor;
    TexCoord = aTexCoord;
}

我们获取顶点对应的纹理坐标以后将其传入片段着色器,利用采样器sampler获取对应坐标的纹理采样:

#version 330 core
out vec4 FragColor;

in vec3 ourColor;
in vec2 TexCoord;

uniform sampler2D ourTexture;

void main()
{
    FragColor = texture(ourTexture, TexCoord);
}
    //顶点输入
    float vertices[] = {
        //     ---- 位置 ----       ---- 颜色 ----     - 纹理坐标 -
             0.5f,  0.5f, 0.0f,   1.0f, 0.0f, 0.0f,   1.0f, 1.0f,   // 右上
             0.5f, -0.5f, 0.0f,   0.0f, 1.0f, 0.0f,   1.0f, 0.0f,   // 右下
            -0.5f, -0.5f, 0.0f,   0.0f, 0.0f, 1.0f,   0.0f, 0.0f,   // 左下
            -0.5f,  0.5f, 0.0f,   1.0f, 1.0f, 0.0f,   0.0f, 1.0f    // 左上
    };

修改了输入的顶点,自然也要在设置顶点属性的时候做一些修改:

    glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 8 * sizeof(float), (void*)0);
    glEnableVertexAttribArray(0);//启用位置0顶点属性

    glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, 8 * sizeof(float), (void*)(3 * sizeof(float)));
    glEnableVertexAttribArray(1);//启用颜色顶点属性

    glVertexAttribPointer(2, 2, GL_FLOAT, GL_FALSE, 8 * sizeof(float), (void*)(6 * sizeof(float)));
    glEnableVertexAttribArray(2);//启用纹理坐标顶点属性

那么就到了最后的渲染时刻,每次渲染前,绑定一次纹理即可

		ourShader.use();
        glBindTexture(GL_TEXTURE_2D, texture);//绑定纹理,自动把纹理赋值给片段着色器的采样器
        glBindVertexArray(VAO); // 一般每次使用时再绑定,这里只有一个VAO,所以实际上可以不绑
        glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, 0);

效果:
在这里插入图片描述

1.2 两个纹理混合

如果我们一次要用到多个纹理怎么办?这时候之前片段着色器里texture的uniform修饰就派上用场了,我们可以定义两个纹理单元,然后用mix函数定义纹理颜色为texture1的80%和texture2的20%:

#version 330 core
out vec4 FragColor;

in vec3 ourColor;
in vec2 TexCoord;

uniform sampler2D texture1;
uniform sampler2D texture2;

void main()
{
    FragColor = mix(texture(texture1, TexCoord), texture(texture2, TexCoord), 0.2);
}

接下来我们分别创建绑定两个纹理,注意在读取和存储jpg格式的笑脸图数据时是用的GL_RGBA格式:

//绑定纹理
    glBindTexture(GL_TEXTURE_2D, texture[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_MIN_FILTER, GL_LINEAR);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);

    stbi_set_flip_vertically_on_load(true);
    // 加载并生成纹理
    int width, height, nrChannels;
    unsigned char* data = stbi_load("container.jpg", &width, &height, &nrChannels, 0);
    if (data)
    {
        glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, width, height, 0, GL_RGB, GL_UNSIGNED_BYTE, data);
        glGenerateMipmap(GL_TEXTURE_2D);
    }
    else
    {
        std::cout << "Failed to load texture" << std::endl;
    }
    stbi_image_free(data);

    glBindTexture(GL_TEXTURE_2D, texture[1]);

    // 为当前绑定的纹理对象设置环绕、过滤方式
    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_MIN_FILTER, GL_LINEAR);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);

    stbi_set_flip_vertically_on_load(true);
    // 加载并生成纹理
    data = stbi_load("awesomeface.png", &width, &height, &nrChannels, 0);
    if (data)
    {
        glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, width, height, 0, GL_RGBA, GL_UNSIGNED_BYTE, data);
        glGenerateMipmap(GL_TEXTURE_2D);
    }
    else
    {
        std::cout << "Failed to load texture" << std::endl;
    }
    stbi_image_free(data);

渲染之前我们要告诉OpenGL每个着色器采样器属于哪个纹理单元。我们只需要设置一次即可,所以这个会放在渲染循环的前面:

    ourShader.use();
    ourShader.setInt("texture1", 0);
    ourShader.setInt("texture2", 1);

渲染中每次我们绑定纹理前要首先激活对应的纹理单元:

        //使用着色器程序
        ourShader.use();
        glActiveTexture(GL_TEXTURE0);
        glBindTexture(GL_TEXTURE_2D, texture[0]);//绑定纹理,自动把纹理赋值给片段着色器的采样器
        glActiveTexture(GL_TEXTURE1);
        glBindTexture(GL_TEXTURE_2D, texture[1]);//绑定纹理,自动把纹理赋值给片段着色器的采样器
        glBindVertexArray(VAO); // 一般每次使用时再绑定,这里只有一个VAO,所以实际上可以不绑
        glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, 0);

效果:
在这里插入图片描述

2. 练习

2.1 修改片段着色器,仅让笑脸图案朝另一个方向看

仅让笑脸纹理坐标左右颠倒即可

#version 330 core
out vec4 FragColor;

in vec3 ourColor;
in vec2 TexCoord;

uniform sampler2D texture1;
uniform sampler2D texture2;

void main()
{
    FragColor = mix(texture(texture1, TexCoord), texture(texture2, vec2(1 - TexCoord.x, TexCoord.y)), 0.2);
}

效果:
在这里插入图片描述

2.2 尝试用不同的纹理环绕方式,设定一个从0.0f到2.0f范围内的(而不是原来的0.0f到1.0f)纹理坐标。试试看能不能在箱子的角落放置4个笑脸

环绕方式为重复,扩大2倍纹理坐标即可:

    //顶点输入
    float vertices[] = {
        //     ---- 位置 ----       ---- 颜色 ----     - 纹理坐标 -
             0.5f,  0.5f, 0.0f,   1.0f, 0.0f, 0.0f,   2.0f, 2.0f,   // 右上
             0.5f, -0.5f, 0.0f,   0.0f, 1.0f, 0.0f,   2.0f, 0.0f,   // 右下
            -0.5f, -0.5f, 0.0f,   0.0f, 0.0f, 1.0f,   0.0f, 0.0f,   // 左下
            -0.5f,  0.5f, 0.0f,   1.0f, 1.0f, 0.0f,   0.0f, 2.0f    // 左上
    };

效果:
在这里插入图片描述

2.3 尝试在矩形上只显示纹理图像的中间一部分,修改纹理坐标,达到能看见单个的像素的效果。尝试使用GL_NEAREST的纹理过滤方式让像素显示得更清晰

缩小纹理坐标即可:

    //顶点输入
    float vertices[] = {
        //     ---- 位置 ----       ---- 颜色 ----     - 纹理坐标 -
             0.5f,  0.5f, 0.0f,   1.0f, 0.0f, 0.0f,   0.1f, 0.1f,   // 右上
             0.5f, -0.5f, 0.0f,   0.0f, 1.0f, 0.0f,   0.1f, 0.0f,   // 右下
            -0.5f, -0.5f, 0.0f,   0.0f, 0.0f, 1.0f,   0.0f, 0.0f,   // 左下
            -0.5f,  0.5f, 0.0f,   1.0f, 1.0f, 0.0f,   0.0f, 0.1f    // 左上
    };

纹理的放大性质选择GL_NEAREST:

    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);

效果:
在这里插入图片描述

2.4 使用一个uniform变量作为mix函数的第三个参数来改变两个纹理可见度,使用上和下键来改变箱子或笑脸的可见度

如题,我们定义一个uniform变量作为mix函数的第三个参数

#version 330 core
out vec4 FragColor;

in vec3 ourColor;
in vec2 TexCoord;

uniform sampler2D texture1;
uniform sampler2D texture2;
uniform vec2 visibility;

void main()
{
    FragColor = mix(texture(texture1, TexCoord), texture(texture2, vec2(1 - TexCoord.x, TexCoord.y)), visibility.x);
}

然后我们在渲染循环里面获取这个变量的位置,一旦检测到按下上下键,就改变它的可见度(因为可见度范围在0-1之间,所以这里要加一个限制)

		int vertexColorLocation = glGetUniformLocation(ourShader.ID, "visibility");

        //使用着色器程序
        ourShader.use();
        if (glfwGetKey(window, GLFW_KEY_UP) == GLFW_PRESS) 
            visi = std::fmin(visi + 0.001, 1);
        else if (glfwGetKey(window, GLFW_KEY_DOWN) == GLFW_PRESS) 
            visi = std::fmax(visi - 0.001, 0);
        glUniform2f(vertexColorLocation, visi, 1.0f);

效果:
在这里插入图片描述

同样的我们也可以用之前提到的随时间变化:

        // 渲染指令
        float timeValue = glfwGetTime();
        float visi = (sin(timeValue) / 2.0f) + 0.5f;
        int vertexColorLocation = glGetUniformLocation(ourShader.ID, "visibility");

        //使用着色器程序
        ourShader.use();
        glUniform2f(vertexColorLocation, visi, 1.0f);

效果:
在这里插入图片描述

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

【OpenGL】笔记五、纹理 的相关文章

随机推荐

  • 幽灵的礼物

    1 幽灵的礼物 前言我是你成功背后的影子 序言在幽灵的礼物中发现金矿 引子分享幽灵的智慧 第一章你是谁 交易圈中的幽灵 第二章备战交易 大师的匠心 第三章规则一 只持有正确的仓位 第四章规则二 正确加码才能获利 第五章规则一加二实战示范 第
  • 如何在Vue项目中使用vw实现移动端适配

    有关于移动端的适配布局一直以来都是众说纷纭 对应的解决方案也是有很多种 在 使用Flexible实现手淘H5页面的终端适配 提出了Flexible的布局方案 随着viewport单位越来越受到众多浏览器的支持 因此在 再聊移动端页面的适配
  • 基于TCN- BILSTM时间序列预测Python程序

    基于TCN BILSTM时间序列预测Python程序 特色 1 单变量 多变量输入 自由切换 2 单步预测 多步预测 自动切换 3 基于Pytorch架构 4 多个评估指标 MAE MSE R2 MAPE等 5 数据从excel文件中读取
  • Flutter-Wrap的使用说明

    Flutter Wrap的使用说明 Wrap可以进行水平方向或者垂直方向上的布局 在一行或者一列现实不完所有的widgets的时候 能够根据当前宽度或者高度自动换行 Wrap的定义 Wrap定义的属性不多 查看如下 Wrap Key key
  • Python中日志模块 logging 的使用,模块处理流程和相关类及常用方法

    文章目录 LOG 日志相关概念 logging模块 logging模块级别的日志 longging模块的处理流程 logging日志模块相关类及其常用方法 Logger类 Handler类 Formater类 Filter类 logging
  • 使用vue+electron创建桌面软件(一)

    使用vue项目 添加electron 创建桌面软件 软件的本地运行 打包等功能 环境配置 node环境 开发环境 linux vue vli 若没有vue环境 则终端安装 npm install g vue cli 查看vue cli版本
  • conversion from ‘QList<QByteArray>‘ to non-scalar type ‘QStringList‘ requested

    conversion from QList to non scalar type QStringList requested 如此写 报错 QStringList tempData textStream readLine split 改为
  • netcat 的使用教程

    软件准备 windows访问 netcat 1 11 for Win32 Win64 下载 linux apt install netcat 命令格式 nc 参数 主机名称 端口 参数 nc h c
  • SVN 提交操作

    在上一章中 我们检出了版本库runoob01 对应的目录放在 home user01 runoob01中 下面我们针对这个库进行版本控制 我们在库本版中需要增加一个readme的说明文件 root runoob svn runoob01 t
  • Quartz框架支持分布式任务处理

    一 问题分析 对于单机的任务调度 使用Quartz十分方便 但是在分布式情况下 对于集群中每台机器都会执行任务 从而造成了重复执行任务的问题 二 解决思路 Quart不仅支持单机任务调度 同时也支持集群中的任务调度 原理如下 在集群中 各个
  • 虚拟化(KVM)介绍以及网卡配置

    虚拟化 KVM 虚拟化介绍 VMware Workstation就是虚拟化 虚拟化简单讲 就是把一台物理计算机虚拟成多台逻辑计算机 每个逻辑计算机里面可以运行不同的操作系统 相互不受影响 这样就可以充分利用硬件资源 关键词Hyperviso
  • 需要打开多少监视器

    import java util Scanner public class B 1 public static int directions 1 0 1 0 0 1 0 1 四行两列 public static void main Stri
  • 汽车行业数据备份有必要吗?

    随着电气化 智能化 网联化和数字化的突破性发展 汽车产业供应链进一步重新构筑 更多科技型企业 汽车供应商以不同形式加入到整车领域 促使中国汽车产业发展进入崭新阶段 在疫情挑战下逐步实现恢复和增长 在数字化时代的大趋势下 数据安全成为全球企业
  • 什么是过拟合和欠拟合,怎么解决?

    过拟合和欠拟合的解释 欠拟合是指模型在训练集 验证集和测试集上均表现不佳的情况 过拟合是指模型在训练集上表现很好 到了验证和测试阶段就很差 即模型的泛化能力很差 过拟合和欠拟合产生的原因 欠拟合 underfitting 模型复杂度过低 特
  • 青藤放飞“猎鹰”,主动防御又多一张牌

    点击上方关注我们 研习ATT CK 模拟安全攻防大战 这一切只要在牌桌上就能完成 在10月30日举行的青藤新品 全国巡展 北京站 现场 就进行了一场别开生面的青藤 首届ATT CK卡牌争霸赛 将专业的安全知识融入卡牌游戏 这个创意棒棒的 让
  • enumerate的用法

    for i data in enumerate trainloader 0 data里面包含图像数据 inputs tensor类型的 和标签 labels tensor类型 inputs labels data enumerate 用于可
  • L2TP and PPTP共存一键安装

    一 L2TP IPSec vpn一键安装脚本 运行下面的命令 wget no check certificate https raw githubusercontent com teddysun across master l2tp sh
  • 批量执行python程序文件

    如果想执行n个文件 不必一个一个点run 可以把要执行的文件放在同一个文件夹里 然后在一个文件里输入以下脚本即可 import os lst os listdir os getcwd 获取当前目录下所有的文件名 for c in lst i
  • BT656跟BT1120和BT709有什么区别

    如果你认为本系列文章对你有所帮助 请大家有钱的捧个钱场 点击此处赞助 赞助额1元起步 多少随意 锋影 email 174176320 qq com 601是SDTV的数据结构 656是SDTV的interface709是HDTV的数据结构
  • 【OpenGL】笔记五、纹理

    1 流程 1 1 单个纹理 纹理是一个2D图片 甚至也有1D和3D的纹理 它可以用来添加物体的细节 为了能够使用纹理图片 我们需要一个叫做stb image h的头文件库来加载不同格式的图片作为纹理 全部文件 得到该头文件后 加入项目 并且