2021SC@SDUSC
现实生活中,纹理最通常的作用是装饰我们的物体模型,它就像是贴纸一样贴在物体表面,使得物体表面拥有图案。但实际上在渲染器中,纹理的作用不仅限于此,它还可以用来存储大量的数据,例如利用纹理存储地形信息。
材质是一个数据集,主要功能就是给渲染器提供数据和光照算法。贴图就是其中数据的一部分,根据用途不同,贴图也会被分成不同的类型。
材质包含贴图,贴图包含纹理,纹理是最基本的数据输入单位。贴图的英语 Map 包含的一层含义就是“映射”,其功能就是把纹理通过 UV 坐标映射到3D物体表面。贴图包含了除了纹理以外其他很多信息,比方说 UV 坐标、贴图输入输出控制等等。
两种方法:
- 来自数据:纹理映射——从二维图像读取颜色或其他信息
- 程序纹理:着色器-编写小型程序,计算颜色/信息作为位置的函数
UV坐标
为了能够把纹理映射到三角形面片上,我们需要指定三角形的每个顶点各自对应纹理的哪个部分。这样每个顶点就每个顶点P存储二维的(u,v)“纹理坐标”,UV确定顶点纹理的二维平面位置。之后在图形的其它片段上进行片段插值。
获得UV坐标:
- 手动由用户提供
- 自动使用参数化优化
- 数学映射(不一定需要每个顶点)
纹理UV优化目标:将3D物体“扁平”到二维UV坐标上
- 对于每个顶点,找到坐标U、V,使畸变最小化
- UV中的距离对应于网格上的距离,3D三角形的角度与UV平面中的三角形角相同
- 通常需要切割(不连续性)
纹理坐标在x和y轴上,范围为0到1之间。使用纹理坐标获取纹理颜色叫做采样。纹理坐标起始于(0, 0),也就是纹理图片的左下角,终始于(1, 1),即纹理图片的右上角。我们希望三角形的左下角对应纹理的左下角,因此我们把三角形左下角顶点的纹理坐标设置为(0, 0);三角形的上顶点对应于图片的上中位置所以我们把它的纹理坐标设置为(0.5,1.0);同理右下方的顶点设置为(1, 0),我们只要给顶点着色器传递这三个纹理坐标就行了,接下来它们会被传片段着色器中,它会为每个片段进行纹理坐标的插值。
举一个简单的例子,纹理坐标可以这样表示:
float vertices[] = {
0.5f, 0.5f, 0.0f, // 右上角
0.5f, -0.5f, 0.0f, // 右下角
-0.5f, -0.5f, 0.0f, // 左下角
-0.5f, 0.5f, 0.0f // 左上角
};
unsigned int indices[] = {
// 索引从0开始
0, 1, 3, // 第一个三角形
1, 2, 3 // 第二个三角形
};
使用纹理
使用纹理的步骤:
- 指定纹理
- 读取或生成图像
- 分配给纹理
- 启用纹理
- 将纹理坐标分配给顶点
- 指定纹理参数:包装、过滤、Mipmap
glGenTextures(1, &texture);
glBindTexture(GL_TEXTURE_2D, texture);
glTexImage2D(GL_TEXTURE_2D, 0, GL_DEPTH_COMPONENT,
SHADOW_WIDTH, SHADOW_HEIGHT, 0, GL_DEPTH_COMPONENT, GL_FLOAT, nullptr);
// 为当前绑定的纹理对象设置环绕、过滤方式
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_BORDER);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_BORDER);
- glGenTextures函数首先需要输入生成纹理的数量,然后把它们储存在第二个参数的unsigned int数组中,就像其他对象一样,我们需要绑定它,让之后任何的纹理指令都可以配置当前绑定的纹理
- glBindTexture函数调用会绑定这个纹理到当前激活的纹理单元,纹理单元GL_TEXTURE0默认总是被激活
- 通过glTexImage2D函数来生成纹理,当调用glTexImage2D时,当前绑定的纹理对象就会被附加上纹理图像
使用纹理坐标更新顶点数据:
float vertices[] = {
// ---- 位置 ---- ---- 颜色 ---- - 纹理坐标 -
0.5f, 0.5f, 0.0f, 1.0f, 0.0f, 0.0f, 1.0f, 1.0f, // 右上
0.5f, -0.5f, 0.0f, 0.0f, 1.0f, 0.0f, 1.0f, 0.0f, // 右下
-0.5f, -0.5f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, // 左下
-0.5f, 0.5f, 0.0f, 1.0f, 1.0f, 0.0f, 0.0f, 1.0f // 左上
};
Mipmap
假设我们有一个包含着上千物体的大房间,每个物体上都有纹理。有些物体会很远,但其纹理会拥有与近处物体同样高的分辨率。由于远处的物体可能只产生很少的片段,OpenGL从高分辨率纹理中为这些片段获取正确的颜色值就很困难,因为它需要对一个跨过纹理很大部分的片段只拾取一个纹理颜色。在小物体上这会产生不真实的感觉,更不用说对它们使用高分辨率纹理浪费内存的问题了。
如何解决走样:多级渐远纹理 Mipmap
简单来说就是建立一系列的纹理图像,后一个纹理图像是前一个的二分之一。把一张贴图按照2的倍数进行缩小,直到1X1,把缩小的图都存储起来。在渲染时,根据一个像素离眼睛位置的距离,来判断从一个合适的图层中取出颜色赋值给像素。其中,根据原图像建立层,每层存储量是上一层的1/4,根据区域大小查找相对应的层,通过插值计算出颜色值(非整数层需在层与层之间做插值计算)。
bool TexturePainter::paintStroke(QPainter &painter, const TexturePainterStroke &stroke)
{
size_t targetTriangleIndex = 0;
if (!intersectRayAndPolyhedron(stroke.mouseRayNear,
stroke.mouseRayFar,
m_context->object->vertices,
m_context->object->triangles,
m_context->object->triangleNormals,
&m_targetPosition,
&targetTriangleIndex)) {
return false;
}
if (PaintMode::None == m_paintMode)
return false;
if (nullptr == m_context->colorImage) {
qDebug() << "TexturePainter paint color image is null";
return false;
}
const std::vector<std::vector<QVector2D>> *uvs = m_context->object->triangleVertexUvs();
if (nullptr == uvs) {
qDebug() << "TexturePainter paint uvs is null";
return false;
}
const std::vector<std::pair<QUuid, QUuid>> *sourceNodes = m_context->object->triangleSourceNodes();
if (nullptr == sourceNodes) {
qDebug() << "TexturePainter paint source nodes is null";
return false;
}
const std::map<QUuid, std::vector<QRectF>> *uvRects = m_context->object->partUvRects();
if (nullptr == uvRects)
return false;
const auto &triangle = m_context->object->triangles[targetTriangleIndex];
QVector3D coordinates = barycentricCoordinates(m_context->object->vertices[triangle[0]],
m_context->object->vertices[triangle[1]],
m_context->object->vertices[triangle[2]],
m_targetPosition);
double triangleArea = areaOfTriangle(m_context->object->vertices[triangle[0]],
m_context->object->vertices[triangle[1]],
m_context->object->vertices[triangle[2]]);
auto &uvCoords = (*uvs)[targetTriangleIndex];
QVector2D target2d = uvCoords[0] * coordinates[0] +
uvCoords[1] * coordinates[1] +
uvCoords[2] * coordinates[2];
double uvArea = areaOfTriangle(QVector3D(uvCoords[0].x(), uvCoords[0].y(), 0.0),
QVector3D(uvCoords[1].x(), uvCoords[1].y(), 0.0),
QVector3D(uvCoords[2].x(), uvCoords[2].y(), 0.0));
double radiusFactor = std::sqrt(uvArea) / std::sqrt(triangleArea);
std::vector<QRect> rects;
const auto &sourceNode = (*sourceNodes)[targetTriangleIndex];
auto findRects = uvRects->find(sourceNode.first);
const int paddingSize = 2;
if (findRects != uvRects->end()) {
for (const auto &it: findRects->second) {
if (!it.contains({target2d.x(), target2d.y()}))
continue;
rects.push_back(QRect(it.left() * m_context->colorImage->height() - paddingSize,
it.top() * m_context->colorImage->height() - paddingSize,
it.width() * m_context->colorImage->height() + paddingSize + paddingSize,
it.height() * m_context->colorImage->height() + paddingSize + paddingSize));
break;
}
}
QRegion clipRegion;
if (!rects.empty()) {
std::sort(rects.begin(), rects.end(), [](const QRect &first, const QRect &second) {
return first.top() < second.top();
});
clipRegion.setRects(&rects[0], rects.size());
painter.setClipRegion(clipRegion);
}
double radius = m_radius * radiusFactor * m_context->colorImage->height();
QVector2D middlePoint = QVector2D(target2d.x() * m_context->colorImage->height(),
target2d.y() * m_context->colorImage->height());
QRadialGradient gradient(QPointF(middlePoint.x(), middlePoint.y()), radius);
gradient.setColorAt(0.0, m_brushColor);
gradient.setColorAt(1.0, Qt::transparent);
painter.fillRect(middlePoint.x() - radius,
middlePoint.y() - radius,
radius + radius,
radius + radius,
gradient);
return true;
}