50、STL与图形学:顶点管理、矩阵运算、渲染管线中的STL应用
图形学,听起来挺高大上的对吧?其实说白了,就是跟一堆数字打交道——顶点坐标、变换矩阵、颜色值……而这些数字的存储和组织,恰恰是STL最擅长的事。我早年做一个小型软件渲染器的时候,一开始没用好STL,代码写得又臭又长。后来重构了一遍,用vector、array、valarray这些容器一换,整个世界都清爽了。
今天我们就聊聊,在图形学的几个核心场景里,STL到底能怎么帮我们省事。
顶点管理:std::vector 与结构体布局
顶点是图形学里最基础的数据单元。一个顶点通常包含位置、法线、纹理坐标等信息。你想想看,如果每个顶点都单独new出来,那性能得多差?
我个人习惯用 std::vector 来管理顶点数组。而且我建议你把顶点定义成一个结构体,而不是分开存几个vector。
struct Vertex {
float position[3]; // x, y, z
float normal[3]; // nx, ny, nz
float texcoord[2]; // u, v
};
std::vector<Vertex> vertices;
vertices.reserve(100000); // 预分配,避免频繁扩容
这样做的好处是内存局部性好。GPU在读取顶点数据时,一次内存访问就能拿到一个顶点的全部信息。我在项目中遇到过有人把position、normal、texcoord分别存在三个vector里,结果渲染时CPU端要来回跳着读,性能直接掉了30%。
std::deque 或者 std::list,但记得——list的节点分散存储,对缓存不友好。
矩阵运算:std::array 与 std::valarray
矩阵是图形学的灵魂。平移、旋转、缩放、投影……全是矩阵乘法。很多人一上来就用二维vector表示矩阵,比如 std::vector<std::vector<float>>。嗯,这里要注意:这种方式性能极差,因为每一行都是一个独立的内存块,访问起来全是随机跳。
我建议用 std::array 或者一维 std::vector 来存储矩阵元素。4x4矩阵就16个float,用 std::array<float, 16> 最合适,栈上分配,没有堆开销。
using Mat4 = std::array<float, 16>;
Mat4 identity = {
1,0,0,0,
0,1,0,0,
0,0,1,0,
0,0,0,1
};
// 矩阵乘法
Mat4 multiply(const Mat4& a, const Mat4& b) {
Mat4 result = {};
for (int row = 0; row < 4; ++row) {
for (int col = 0; col < 4; ++col) {
float sum = 0;
for (int k = 0; k < 4; ++k) {
sum += a[row * 4 + k] * b[k * 4 + col];
}
result[row * 4 + col] = sum;
}
}
return result;
}
如果你要做大量的逐元素运算,比如向量点乘、矩阵逐元素加减,std::valarray 是个好东西。它支持类似MATLAB的批量运算语法。我曾经用它写过一段法线贴图的扰动计算,代码量比手写循环少了一半。
std::valarray<float> vecA = {1.0, 2.0, 3.0};
std::valarray<float> vecB = {4.0, 5.0, 6.0};
std::valarray<float> result = vecA + vecB; // {5, 7, 9}
float dot = (vecA * vecB).sum(); // 1*4 + 2*5 + 3*6 = 32
std::valarray 做矩阵乘法,记得配合 std::slice 来提取行和列。虽然语法有点绕,但熟悉之后写起来很爽。
渲染管线中的STL应用
渲染管线说白了就是一条流水线:顶点进来,经过一系列变换,最后变成屏幕上的像素。这条流水线的每个阶段,STL都能派上用场。
1. 顶点缓冲与索引缓冲
现代图形API(如OpenGL、Vulkan)都要求你把顶点数据上传到GPU。STL的vector可以直接拿来当CPU端的临时缓冲。我习惯这样写:
std::vector<Vertex> cpuVertexBuffer;
// ... 填充数据 ...
glGenBuffers(1, &vbo);
glBindBuffer(GL_ARRAY_BUFFER, vbo);
glBufferData(GL_ARRAY_BUFFER,
cpuVertexBuffer.size() * sizeof(Vertex),
cpuVertexBuffer.data(),
GL_STATIC_DRAW);
注意 .data() 这个成员函数,它返回底层数组的指针,可以直接传给OpenGL。这一点很多新手不知道,还在那手动 &cpuVertexBuffer[0],其实效果一样,但 .data() 语义更清晰。
2. 视锥体裁剪
渲染前,我们通常要剔除那些不在相机视野内的物体。这时候 std::erase_if(C++20)就很好用:
std::vector<Mesh> visibleMeshes = allMeshes;
std::erase_if(visibleMeshes, [&](const Mesh& m) {
return !isInsideFrustum(m.boundingSphere, cameraFrustum);
});
我曾经在项目里用这个做场景剔除,配合 std::partition 把可见和不可见的物体分开,比手写循环快了不少。因为STL的算法内部做了很多优化,比如循环展开、SIMD指令等。
3. 排序与透明度
透明物体的渲染需要从远到近排序。这时候 std::sort 配上自定义比较器:
std::sort(transparentObjects.begin(), transparentObjects.end(),
[&](const RenderItem& a, const RenderItem& b) {
return a.distanceToCamera > b.distanceToCamera;
});
嗯,这里要注意:如果物体数量不多(比如几十个),用 std::sort 完全没问题。但如果场景里有成千上万个透明物体,排序开销会很大。我建议先做一次粗略的桶排序,再用 std::sort 做精细排序。
知识体系总览
下面这张图概括了STL在图形学中的几个主要应用场景。你可以看到,从数据存储到算法处理,STL几乎覆盖了渲染管线的每个环节。
避坑指南
最后分享几个我踩过的坑:
- vector
别用 —— 它被特化成了位压缩版本,取引用时返回的是代理对象,不能直接取地址。如果你要存bool数组,用std::vector<char>或者std::bitset。 - 频繁push_back记得reserve —— 我见过一个项目,每帧往vector里push 10万个顶点,没做reserve,结果每帧都在内存拷贝,帧率直接掉到个位数。
- std::valarray 的切片操作 —— 语法比较反直觉,建议先写个小demo测试通过再集成到项目里。我曾经因为slice参数写反了,调试了整整一个下午。
核心总结: STL不是为图形学量身定做的,但它提供的数据结构和算法,恰好能覆盖图形学中80%的日常需求。用好vector、array、valarray,配合erase_if、sort等算法,你的渲染代码会既清晰又高效。
公众号:蓝海资料掘金营,微信deep3321