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.效果图

相关推荐
用户805533698031 天前
不止三件套:QObject 属性系统全关键字与运行时反射!
c++·qt
xcyxiner1 天前
DicomViewer (vcpkg Windows和ubuntu编译)7
qt
Quz6 天前
QML Hello World 入门示例
qt
xcyxiner9 天前
DicomViewer (dcmtk读取dcm文件)5
qt
xcyxiner10 天前
DicomViewer (后台线程处理文件)4
qt
xcyxiner10 天前
DicomViewer (添加模型类)3
qt
xcyxiner11 天前
DicomViewer (目录调整) 2
qt
xcyxiner11 天前
dcmtk vtk vtk-dicom(gdcm) 编译(debug) v2
qt
桥田智能13 天前
桥田智能 QT-650S:面向白车身焊装的 800kg 重载快换解决方案
开发语言·qt·系统架构
森G13 天前
75、服务器源码解析---------云视频服务项目
linux·服务器·网络·c++·qt