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







