一.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.构造函数:
cpp// 创建空纹理(2D纹理最常用) QOpenGLTexture(QOpenGLTexture::Target2D);
Target2D= 2D 纹理(2D场景用这个)- 其他:
TargetCubeMap立方体贴图、Target1D一维纹理
b.加载图像数据:
cpp// 从 QImage 创建纹理 void setData(const QImage &image, QOpenGLTexture::PixelFormat = PixelAuto, QOpenGLTexture::PixelType = PixelTypeUInt8);用法:
cppQImage img(":/texture.png"); texture->setData(img);
c. 纹理绑定(激活纹理单元)
cpp// 绑定当前纹理,让着色器能读到 void bind(); // 绑定到指定纹理单元(多纹理用) void bind(uint unit);示例:
cpptexture->bind(0); // 绑定到 0 号纹理单元
d. 纹理参数设置
① 纹理过滤(放大 / 缩小)
cpp// 缩小过滤(纹理比显示区域小) setMinificationFilter(Filter); // 放大过滤(纹理比显示区域大) setMagnificationFilter(Filter);常用过滤模式:
Linear:线性过滤(模糊,平滑,推荐)Nearest:最近点采样(像素风)② 纹理环绕方式(超出 [0,1] 坐标时)
cppsetWrapMode(WrapDirection, WrapMode);**arg1.WrapDirection:**指定要设置哪个轴的环绕方式
方向常量 说明 QOpenGLTexture::DirectionSS方向(对应U轴/纹理宽度方向/X轴) QOpenGLTexture::DirectionTT方向(对应V轴/纹理高度方向/Y轴) QOpenGLTexture::DirectionRR方向(对应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(边界颜色)
效果:超出部分显示指定的边界颜色
适用:需要透明边界或特定背景色
例如:
cpptexture->setWrapMode(QOpenGLTexture::ClampToBorder); texture->setBorderColor(QColor(0,0,0,0)); // 设置透明边界
e. 生成 Mipmap(高清纹理优化)
cpptexture->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.编写纹理着色器代码
cppconst 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
cppQOpenGLVertexArrayObject *vao=new QOpenGLVertexArrayObject(this); QOpenGLBuffer *vbo=new QOpenGLBuffer(QOpenGLBuffer::VertexBuffer);//负责顶点数据VBO的作用就是把数据传给GPU,前面几张是传顶点坐标+顶点颜色,现在是传顶点坐标+纹理坐标。
b.设置顶点指针属性
设置顶点指针属性并开通通道,让GPU知道怎么去处理这段数据,然后给纹理着色器跑。
cppglVertexAttribPointer(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.设置清空颜色+清空
cppglClearColor(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.绑定纹理着色器程序
cppshaderProgram->bind();d.绑定vao
cppvao->bind();e.开始绘制
cppglDrawArrays
四:第一个纹理渲染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.效果图
