欢迎光临
我们一直在努力

4.纹理贴图 Texture

一.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 核心作用

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

    a.构造函数:

    // 创建空纹理(2D纹理最常用)
    QOpenGLTexture(QOpenGLTexture::Target2D);

    • Target2D = 2D 纹理(2D场景用这个)
    • 其他:TargetCubeMap立方体贴图、Target1D一维纹理
    b.加载图像数据:

    // 从 QImage 创建纹理
    void setData(const QImage &image,
    QOpenGLTexture::PixelFormat = PixelAuto,
    QOpenGLTexture::PixelType = PixelTypeUInt8);

    用法:

    QImage img(":/texture.png");
    texture->setData(img);

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

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

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

    示例:

    texture->bind(0); // 绑定到 0 号纹理单元

    d. 纹理参数设置

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

    // 缩小过滤(纹理比显示区域小)
    setMinificationFilter(Filter);

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

    常用过滤模式:

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

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

    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)(取小数部分)

    • 适用:瓷砖、重复图案

    纹理坐标:0.0 → 1.0 → 2.0 → 3.0
    显示效果:[图][图][图][图]

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

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

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

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

    纹理坐标:0.0 → 1.0 → 2.0 → 3.0
    显示效果:[图][镜像][图][镜像]

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

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

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

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

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

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

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

    • 例如:

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

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

    texture->generateMipMaps();

    • 自动生成多级纹理,远处物体不闪烁
    • 必须在setData()之后调用
    f. 释放与销毁

    // 解绑
    texture->release();

    // 释放GPU显存
    delete texture;

    g.实用工具函数补充

    //获取纹理宽,高
    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.创建纹理采样器

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

    b.上传纹理图片

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

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

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

    GPU纹理存储内部结构图:

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

    c.设置纹理参数

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

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

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

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

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

    step2:创建着色器

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

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

    区别于前面章节:

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

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

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

    超形象比喻:

    你给 GPU说:

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

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

    注意:

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

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

            纹理 = 底图(固定不变)

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

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

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

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

            纹理 = 角色图片

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

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

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

            比如:

            图片是一张白底卡片

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

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

            比如地形:

                    纹理 = 草地贴图

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

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

      (5)…….

    b.编写纹理着色器代码

    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

    QOpenGLVertexArrayObject *vao=new QOpenGLVertexArrayObject(this);

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

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

    b.设置顶点指针属性

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

    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.设置清空颜色+清空

    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多个纹理单元?

    // 绑定纹理1 → 纹理单元0
    texture1->bind(0);

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

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

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

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

    // 片元着色器
    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.绑定纹理着色器程序

    shaderProgram->bind();

    d.绑定vao

    vao->bind();

    e.开始绘制

    glDrawArrays

    四:第一个纹理渲染Demo

    1.代码

    GLWidget.h

    #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

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

    赞(0)
    未经允许不得转载:171主机测评 » 4.纹理贴图 Texture
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址