4.OpenGL纹理贴图

一.OpenGL纹理坐标

OpenGL顶点坐标,OpenGL纹理坐标,图片文件坐标系 (如JPEG、PNG)是三个不同的坐标系。

1. OpenGL 顶点坐标

作用:决定几何体在屏幕上画在哪里(位置)。

  • 范围:x, y, z 都在 [-1, 1]

  • 原点:屏幕正中心

  • 方向:

    • X 轴:向右为正

    • Y 轴:向上为正

    • Z 轴:向外为正

2. OpenGL 纹理坐标

作用:决定图片的哪一部分贴到几何体上。(也叫uv坐标)

  • 范围:[0, 1](归一化,和图片分辨率无关)
  • 原点:左下角 (0,0)
  • 方向:
    • U → 向右
    • V → 向上

3. 图片文件坐标系

真实图片(PNG/JPG/BMP)在内存 / 文件里的存储顺序

  • 原点:左上角 (0,0)
  • 方向:
    • X 向右
    • Y 向下

这就是纹理贴图最常见的坑:上下颠倒的根源。所以使用纹理加载图片时,需要对图片做一个垂直方向翻转(上下颠倒),可以看下面这个模拟图很直观:

纹理:
y

|

0,0------x

图片:

0,0------x

|

y

二:QOpenGLTexture

QOpenGLTexture 是 Qt 封装的OpenGL 纹理操作类,专门用来加载图片、创建纹理、绑定纹理、设置纹理参数,不用手写原生 OpenGL 的glTexImage2D等复杂 API。

1.QOpenGLTexture 核心作用

  1. 加载图片(PNG/JPG/BMP)生成 OpenGL 纹理
  2. 管理纹理 ID、格式、大小
  3. 设置纹理过滤、环绕方式
  4. 绑定纹理到着色器采样器

2.核心函数

a.构造函数:

cpp 复制代码
// 创建空纹理(2D纹理最常用)
QOpenGLTexture(QOpenGLTexture::Target2D);
  • Target2D = 2D 纹理(2D场景用这个)
  • 其他:TargetCubeMap立方体贴图、Target1D一维纹理
    b.加载图像数据:
cpp 复制代码
// 从 QImage 创建纹理
void setData(const QImage &image,
             QOpenGLTexture::PixelFormat = PixelAuto,
             QOpenGLTexture::PixelType = PixelTypeUInt8);

用法:

cpp 复制代码
QImage img(":/texture.png");
texture->setData(img);

c. 纹理绑定(激活纹理单元)

cpp 复制代码
// 绑定当前纹理,让着色器能读到
void bind();

// 绑定到指定纹理单元(多纹理用)
void bind(uint unit);

示例:

cpp 复制代码
texture->bind(0); // 绑定到 0 号纹理单元

d. 纹理参数设置

① 纹理过滤(放大 / 缩小)

cpp 复制代码
// 缩小过滤(纹理比显示区域小)
setMinificationFilter(Filter);

// 放大过滤(纹理比显示区域大)
setMagnificationFilter(Filter);

常用过滤模式:

  • Linear:线性过滤(模糊,平滑,推荐)
  • Nearest:最近点采样(像素风)

② 纹理环绕方式(超出 [0,1] 坐标时)

cpp 复制代码
setWrapMode(WrapDirection, WrapMode);

**arg1.WrapDirection:**指定要设置哪个轴的环绕方式

方向常量 说明
QOpenGLTexture::DirectionS S方向(对应U轴/纹理宽度方向/X轴)
QOpenGLTexture::DirectionT T方向(对应V轴/纹理高度方向/Y轴)
QOpenGLTexture::DirectionR R方向(对应3D纹理的深度方向/Z轴)

注意:对于2D纹理,通常只设置 S 和 T 方向。默认会设置S和T,所以第一个参数可以不填。

arg2.WrapMode:环绕模式

1).QOpenGLTexture::Repeat(重复/平铺)

  • 效果:纹理坐标超出部分重复纹理

  • 公式:coord' = frac(coord)(取小数部分)

  • 适用:瓷砖、重复图案

cpp 复制代码
纹理坐标:0.0 → 1.0 → 2.0 → 3.0
显示效果:[图][图][图][图]

2). QOpenGLTexture::MirroredRepeat(镜像重复)

  • 效果:超出部分镜像翻转后重复

  • 公式:根据整数部分奇偶决定是否翻转

  • 适用:需要无缝连接的重复纹理

cpp 复制代码
纹理坐标:0.0 → 1.0 → 2.0 → 3.0
显示效果:[图][镜像][图][镜像]

3.) QOpenGLTexture::ClampToEdge(边缘拉伸)

  • 效果:超出部分使用边缘像素颜色

  • 适用:避免边缘混叠,大多数3D模型的默认设置

cpp 复制代码
纹理坐标:0.0 → 1.0 → 2.0
显示效果:[图] [边缘像素重复]

4). QOpenGLTexture::ClampToBorder(边界颜色)

  • 效果:超出部分显示指定的边界颜色

  • 适用:需要透明边界或特定背景色

  • 例如:

    cpp 复制代码
    texture->setWrapMode(QOpenGLTexture::ClampToBorder);
    texture->setBorderColor(QColor(0,0,0,0));  // 设置透明边界

e. 生成 Mipmap(高清纹理优化)

cpp 复制代码
texture->generateMipMaps();
  • 自动生成多级纹理,远处物体不闪烁
  • 必须在setData()之后调用
    f. 释放与销毁
cpp 复制代码
// 解绑
texture->release();

// 释放GPU显存
delete texture;

g.实用工具函数补充

cpp 复制代码
//获取纹理宽,高
int width() const;
int height() const;

//设置纹理是否使用透明通道
//1.带透明 PNG 必须用RGBA8
//2.不透明图片用RGB8
texture->setFormat(QOpenGLTexture::RGBA8_UNorm);

//直接设置纹理数据(内存数组)
//用于生成程序纹理(如噪声图、颜色图)
setData(Level, Xoffset, Yoffset, Width, Height, Data, Format, Type);

三:整体流程(核心)

step1:纹理数据准备

a.创建纹理采样器

cpp 复制代码
//Target2D:2D图片专属参数
QOpenGLTexture *t=new QOpenGLTextur(QOpenGLTexture::Target2D);

b.设置纹理参数

这一步是可选,但强烈建议设置,不然会出现显示异常,拉伸模糊、边缘锯齿等情况。

以下三行代码是最常用的纹理配置:

cpp 复制代码
//纹理缩小时怎么采样:三线性过滤(最平滑的缩小模式)
//效果:物体离远了、变小了,纹理不会闪烁、不会锯齿
t->setMinificationFilter(QOpenGLTexture::LinearMipMapLinear);

//纹理放大时怎么采样:线性过滤(平滑放大)
//效果:纹理被拉近、放大时,不会出现马赛克、像素块
t->setMagnificationFilter(QOpenGLTexture::Linear);

//纹理坐标超出 0~1 范围时怎么办:边缘拉伸(最常用、最安全)
//效果:超出范围 = 拉伸边缘颜色,不重复、不黑边
t->setWrapMode(QOpenGLTexture::ClampToEdge);

c.上传纹理图片

cpp 复制代码
//QImage::flipped(Qt::Vertical)  让图片上下翻转
//让图片坐标适配纹理坐标
t->setData(QImage("xxx").flipped(Qt::Vertical));

//低版本用QImage(xxx").mirrored(false,true)

setData(QImage) 是 Qt 的超级全自动函数

它内部会自动检查:

  • 有没有纹理 ID?→ 没有 → 自动调用 create (),所以前面没有调用create
  • 有没有分配显存?→ 没有 → 自动 allocateStorage ()
  • 自动为纹理设置尺寸大小

它不会自动调整:

  • 图像尺寸,如果上次自动设置的尺寸大小与下次不符合,就必须销毁之前的纹理对象,然后再重新创建,因为allocateStorage调用后就不可以再设置再设置纹理的尺寸了。

setData()执行后纹理图片数据就已经上传到了GPU了,在这个过程中:

**采样器执行了:**生成纹理ID------>绑定纹理------>上传数据到GPU------>自动处理格式转换(QImage-OpenGL能懂的格式)

GPU纹理存储内部结构图:

bash 复制代码
+------------------------------------------+
|            GPU 显卡芯片                   |
|                                          |
|  【GPU 显存 (Video Memory / VRAM)】       |
|                                          |
|  +------------------------------------+  |
|  |    纹理存储区 (Texture Memory)     |  | <--- 你的图片就在这里!
|  |                                    |  |
|  |  [ 纹理1 | 纹理2 | 纹理3 | ... ]   |  | <--- tex->setData() 后存这里
|  |                                    |  |
|  +------------------------------------+  |
|                                          |
|  +------------------------------------+  |
|  |   着色器处理器 (Shader Core)       |  | <--- 运行你的顶点/片元着色器
|  |                                    |  |
|  |   纹理采样单元 (Texture Unit)      |  | <--- 根据纹理坐标取色
|  +------------------------------------+  |
|                                          |(这部分看状态机状态,去哪取,怎么取)
+------------------------------------------+

step2:创建着色器

a.编写顶点和片元着色器

(处理纹理坐标,也可以叫纹理着色器)

区别于前面章节:

**前面:**顶点坐标+顶点颜色,靠顶点自己带颜色

**现在:**顶点坐标+纹理坐标,靠纹理坐标取从纹理图片中取样(取出对应位置的像素)

顶点坐标数量要跟纹理坐标数量相同,这样才知道去哪里取,中间部分GPU会自动做线性插值运算(自动计算中间每一个纹理像素应该用哪个纹理坐标)。

超形象比喻:

你给 GPU说:

  • 左上角:取图片左上角
  • 右上角:取图片右上角
  • 左下角:取图片左下角
  • 右下角:取图片右下角

GPU 看到后:"好嘞,中间的点我自动从左到右、从上到下慢慢过渡取色,整张图就铺满啦!

注意:

显示图片通常就不需要传顶点颜色数据了,因为我们本来就是要显示图片,像素都来自纹理图片里面,自己手动加颜色他们就会混合起来可能导致显示错乱。除非你的目的就是要对纹理图片做一些特殊处理,比如:

(1)给纹理 "染色/滤镜":

纹理 = 底图(固定不变)

顶点颜色 = 滤镜 / 染色(动态改)

最终颜色 = 纹理像素 × 顶点颜色

这是游戏 / UI 里最最常用的优化!

(2)2D 游戏:角色受击闪烁 / 渐变透明

纹理 = 角色图片

顶点颜色 = 控制整体透明度 / 变红

角色受伤时:不改图片,直接把顶点色改成 (1, 0.5, 0.5, 1)纹理立刻变红!

(3)渐变色覆盖在图片上

比如:

图片是一张白底卡片

顶点色设置成从上到下渐变蓝最终效果 = 图片 + 自然渐变叠加

(4)顶点色控制区域化颜色(地形 / 面片)

比如地形:

纹理 = 草地贴图

顶点色 = 控制某个角落变枯黄色

不用切多张图,顶点色直接局部调色。

(5).......

b.编写纹理着色器代码

cpp 复制代码
const char* vertexShader=R"(
    #version 330 core
    layout(location=0)in vec2 aPos;//输入顶点坐标,如果是vec3类型就写vec3
    layout(location=1)in vec2 aTexCoord;//输入纹理坐标
    out vec2 texCoord;//输出纹理坐标
    void main()
    {
        gl_Position=vec4(aPos,0.0,1.0);//最终顶点显示位置类型为vec4
        texCoord=aTexCoord;
    }
)";

const char* fragmentShader=R"(
    #version 330 core
    in vec2 texCoord;//接收顶点着色器传来的纹理坐标
    out vec4 FragColor;//最终输出颜色
    uniform sampler2D ourTexture;//纹理采样器(对应CPU上传的纹理图片)
    void main()
    {
        FragColor=texture(ourTexture,texCoord);//根据纹理坐标,从纹理图片取对应位置的像素
    }
)";

step3:创建顶点缓存对象

a.创建 VAO、VBO

cpp 复制代码
QOpenGLVertexArrayObject *vao=new QOpenGLVertexArrayObject(this);

QOpenGLBuffer *vbo=new QOpenGLBuffer(QOpenGLBuffer::VertexBuffer);//负责顶点数据

VBO的作用就是把数据传给GPU,前面几张是传顶点坐标+顶点颜色,现在是传顶点坐标+纹理坐标。

b.设置顶点指针属性

设置顶点指针属性并开通通道,让GPU知道怎么去处理这段数据,然后给纹理着色器跑。

cpp 复制代码
glVertexAttribPointer(0,2,GL_FLOAT,GL_FALSE,sizeof(float)*4,(void*)0);
glEnableVertexAttribArray(0);

glVertexAttribPointer(1,2,GL_FLOAT,GL_FALSE,sizeof(float)*4,(void*)(2*sizeof(float)));
glEnableVertexAttribArray(1);

step4:开始渲染

a.设置清空颜色+清空

cpp 复制代码
glClearColor(0.1,0.1,0.1,1.0);
glClear(GL_COLOR_BUFFER_BIT);

b.绑定纹理单元

一次只能给【一个纹理单元】bind 一个纹理,但是一个GPU中有多个纹理单元。调用bind就是选择哪个纹理单元。

但是一个着色器可以同时从一个或者多个纹理单元中取纹理数据。

纹理单元不是状态机,所以可以bind多个。

超直白的比喻:

纹理单元 = 插槽

显卡有很多个插槽:

  • 插槽 0
  • 插槽 1
  • 插槽 2
  • ...

纹理 = 卡片

bind() = 把卡片插进插槽

规则:

1)一个插槽,同一时间只能插一张卡;

2)但你有很多插槽,可以插很多卡;

3)着色器可以同时从所有插槽取色

怎么同时bind多个纹理单元?

cpp 复制代码
// 绑定纹理1 → 纹理单元0
texture1->bind(0);  

// 绑定纹理2 → 纹理单元1  
texture2->bind(1);  

// 绑定纹理3 → 纹理单元2
texture3->bind(2);  

当bind()没有填参数时,默认会bind到纹理单元0

着色器中怎么同时使用多张纹理?

cpp 复制代码
// 片元着色器
uniform sampler2D tex0;  // 对应纹理单元0
uniform sampler2D tex1;  // 对应纹理单元1
uniform sampler2D tex2;  // 对应纹理单元2

void main() {
    // 同时取3张图的颜色混合
    vec4 c1 = texture(tex0, uv);
    vec4 c2 = texture(tex1, uv);
    vec4 c3 = texture(tex2, uv);
    
    FragColor = c1 + c2 + c3;//叠加变亮,类似三个投影仪叠到一起的效果,然后同时显示
    //+ 相加 → 变亮、发光、叠加
    //原因:两个数相加 → 结果会更大、更亮
    
    //* 相乘 → 变暗、染色、过滤(最常用、最实用)
    //原因:两个小于 1 的数字相乘 → 结果只会更小、更暗
}

c.绑定纹理着色器程序

cpp 复制代码
shaderProgram->bind();

d.绑定vao

cpp 复制代码
vao->bind();

e.开始绘制

cpp 复制代码
glDrawArrays

四:第一个纹理渲染Demo

1.代码

GLWidget.h

cpp 复制代码
#ifndef GLWIDGET_H
#define GLWIDGET_H

#include <QOpenGLWidget>
#include <QOpenGLShaderProgram>
#include <QOpenGLBuffer>
#include <QOpenGLVertexArrayObject>
#include <QOpenGLFunctions_3_3_Core>
#include <QOpenGLTexture>
class GLWidget : public QOpenGLWidget,private QOpenGLFunctions_3_3_Core
{
public:
    explicit GLWidget(QWidget *parent = nullptr);

    ~GLWidget();
protected:
    void initializeGL() override;
    void resizeGL(int w, int h) override;
    void paintGL() override;

    void initShaders();   //着色器初始化部分
    void initTexture();   //纹理初始化部分
    void initBuffers();   // VBO + VAO 初始化
private:
    QOpenGLShaderProgram *m_shaderProgram;
    QOpenGLTexture *m_texture;
    QOpenGLBuffer *m_vbo;
    QOpenGLVertexArrayObject *m_vao;
};

#endif // GLWIDGET_H

GLWidget.cpp

cpp 复制代码
#include "GLWidget.h"
#include <QDebug>
#include <QImage>
GLWidget::GLWidget(QWidget *parent)
    :QOpenGLWidget(parent)
{}
GLWidget::~GLWidget()
{
    // 自动清理
    m_vbo->destroy();
    delete m_texture;
}
void GLWidget::resizeGL(int w, int h)
{}
void GLWidget::initializeGL()
{
    initializeOpenGLFunctions();
    initShaders();
    initTexture();
    initBuffers();
}
void GLWidget::initTexture()
{
    QImage img("test.jpg");
    if(img.isNull())
    {
        qDebug()<<"图片加载失败";
        return;
    }
    m_texture=new QOpenGLTexture(QOpenGLTexture::Target2D);
    m_texture->setData(img.flipped(Qt::Vertical));
    m_texture->setMinificationFilter(QOpenGLTexture::LinearMipMapLinear);
    m_texture->setMagnificationFilter(QOpenGLTexture::Linear);
    m_texture->setWrapMode(QOpenGLTexture::ClampToEdge);
}
void GLWidget::initShaders()
{
    const char* vertexShader=R"(
        #version 330 core
        layout(location=0)in vec2 aPos;
        layout(location=1)in vec2 aTexCoord;
        out vec2 TexCoord;
        void main(){
            gl_Position=vec4(aPos,0.0,1.0);
            TexCoord=aTexCoord;
        }
    )";
    const char* fragmentShader=R"(
        #version 330 core
        in vec2 TexCoord;
        out vec4 FragColor;
        uniform sampler2D ourTexCoord;
        void main()
        {
            FragColor=texture(ourTexCoord,TexCoord);
        }
    )";

    m_shaderProgram=new QOpenGLShaderProgram(this);
    m_shaderProgram->addShaderFromSourceCode(QOpenGLShader::Vertex,vertexShader);
    m_shaderProgram->addShaderFromSourceCode(QOpenGLShader::Fragment,fragmentShader);
    m_shaderProgram->link();
}

void GLWidget::initBuffers()
{
    float vertex[]={
        -0.5,0.5, 0.0,1.0,
        0.5,0.5,    1.0,1.0,
        0.5,-0.5,   1.0,0.0,
        -0.5,-0.5,  0.0,0.0
    };
    m_vao=new QOpenGLVertexArrayObject(this);
    m_vao->create();
    m_vao->bind();
    m_vbo=new QOpenGLBuffer(QOpenGLBuffer::VertexBuffer);
    m_vbo->create();
    m_vbo->bind();
    m_vbo->allocate(vertex,sizeof(vertex));

    glVertexAttribPointer(0,2,GL_FLOAT,GL_FALSE,sizeof(float)*4,(void*)0);
    glEnableVertexAttribArray(0);

    glVertexAttribPointer(1,2,GL_FLOAT,GL_FALSE,sizeof(float)*4,(void*)(2*sizeof(float)));
    glEnableVertexAttribArray(1);

    m_vao->release();
    m_vbo->release();
}

void GLWidget::paintGL()
{
    glClearColor(0.1,0.1,0.1,1.0);
    glClear(GL_COLOR_BUFFER_BIT);

    m_shaderProgram->bind();
    m_texture->bind();
    m_vao->bind();
    glDrawArrays(GL_TRIANGLE_FAN,0,4);
}

2.效果图

相关推荐
娇娇yyyyyy3 小时前
QT编程(9): QTextEdit
前端·qt
森G4 小时前
18、QFile类---------QT基础
qt
混分巨兽龙某某6 小时前
基于ESP32与Qt Creator的WIFI空间透视项目(代码开源)
qt·嵌入式·esp32·wifi空间透视
A10169330717 小时前
QT数据库(三):QSqlQuery使用
数据库·qt·oracle
森G7 小时前
15、QT的容器类---------QT基础
qt
haiyaoyouyou8 小时前
Qt ElaWidgetTools 编译运行示例
开发语言·qt·qt creator·elaframework·mingw_64
不会写DN9 小时前
如何让两个Go程序远程调用?
开发语言·qt·golang
A.A呐1 天前
【QT第三章】常用控件2
开发语言·qt
笨笨马甲1 天前
Qt 实现三维坐标系的方法
开发语言·qt