如何使用tinygltf库在glb/glTF文件中实现基于顶点的纹理选择?
这是个很常见的需求——glTF规范本身确实没有原生提供"顶点级纹理选择"的内置属性,但我们有几种可靠的方案来实现,结合tinygltf库可以轻松搞定。我结合你的最简示例(双三角正方形+两张纹理)和复杂场景需求,分三种方案讲解:
方案1:拆分多个Primitive(最直观,适合分组清晰的场景)
这是最简单的实现方式:把使用不同纹理的顶点组拆成独立的Primitive,每个Primitive绑定对应纹理的材质。
对于你的双三角示例:
- 第一个三角形的顶点(3个顶点)组成一个Primitive,绑定材质A(对应第一张纹理)
- 第二个三角形的顶点(3个顶点)组成另一个Primitive,绑定材质B(对应第二张纹理)
在tinygltf里的实现步骤:
- 为每个纹理创建独立的
tinygltf::Material对象,设置pbrMetallicRoughness.baseColorTexture.index为对应的纹理索引。 - 为每个顶点组创建独立的
tinygltf::Primitive对象:- 每个Primitive的
attributes包含POSITION、TEXCOORD_0等必要属性,指向对应顶点数据的Accessor。 - 把Primitive的
material字段设置为对应材质的索引。
- 每个Primitive的
- 将这些Primitive加入同一个
tinygltf::Mesh(或者不同Mesh,看需求),最终写入glTF文件。
这种方案的优势是无需自定义着色器,完全兼容glTF标准加载流程;缺点是如果顶点组数量多,会增加Draw Call数量,对性能有轻微影响。
方案2:自定义顶点属性+着色器采样(适合复杂网格,Draw Call友好)
如果你不想拆分太多Primitive,可以通过自定义顶点属性传递纹理索引,配合着色器实现动态纹理选择。
步骤1:添加自定义顶点属性
在tinygltf中,你可以添加一个自定义顶点属性(比如命名为TEXINDEX_0),每个顶点存储对应的纹理索引(用无符号字节或整数类型,节省空间):
// 假设我们有一个存储纹理索引的字节数组:uint8_t texIndices[] = {0,0,0,1,1,1}; tinygltf::Buffer buffer; buffer.data.insert(buffer.data.end(), reinterpret_cast<const char*>(texIndices), reinterpret_cast<const char*>(texIndices) + 6); int bufferIndex = doc.buffers.size(); doc.buffers.push_back(buffer); // 创建BufferView tinygltf::BufferView bufferView; bufferView.buffer = bufferIndex; bufferView.byteOffset = 0; bufferView.byteLength = 6; bufferView.target = TINYGLTF_TARGET_ARRAY_BUFFER; int bufferViewIndex = doc.bufferViews.size(); doc.bufferViews.push_back(bufferView); // 创建Accessor tinygltf::Accessor accessor; accessor.bufferView = bufferViewIndex; accessor.componentType = TINYGLTF_COMPONENT_TYPE_UNSIGNED_BYTE; accessor.count = 6; // 6个顶点 accessor.type = TINYGLTF_TYPE_SCALAR; accessor.minValues = {0}; accessor.maxValues = {1}; int accessorIndex = doc.accessors.size(); doc.accessors.push_back(accessor); // 把自定义属性添加到Primitive的attributes中 primitive.attributes["TEXINDEX_0"] = accessorIndex;
步骤2:编写自定义着色器
需要修改glTF的顶点/片段着色器,使用纹理索引采样对应的纹理数组:
// 顶点着色器 #version 300 es in vec3 POSITION; in vec2 TEXCOORD_0; in uint TEXINDEX_0; // 对应自定义属性 out vec2 v_texCoord; flat out uint v_texIndex; // flat修饰避免插值 void main() { gl_Position = projectionMatrix * modelViewMatrix * vec4(POSITION, 1.0); v_texCoord = TEXCOORD_0; v_texIndex = TEXINDEX_0; } // 片段着色器 #version 300 es precision mediump float; uniform sampler2D u_textures[2]; // 纹理数组 in vec2 v_texCoord; flat in uint v_texIndex; out vec4 FragColor; void main() { FragColor = texture(u_textures[v_texIndex], v_texCoord); }
步骤3:加载时绑定纹理数组
在加载glTF文件后,你需要把tinygltf中对应的纹理对象绑定到不同的纹理单元,然后把纹理单元的位置传入着色器的u_textures uniform数组。
这种方案的优势是只需要一个Primitive,Draw Call最少;缺点是需要自定义着色器,且需要处理纹理数组的绑定逻辑,兼容性略低于方案1。
方案3:纹理图集(Texture Atlas,适合多小纹理场景)
如果你的纹理数量多且尺寸不大,可以把所有纹理合并成一张纹理图集,然后调整每个顶点的UV坐标,让它们指向图集中对应的区域。
实现步骤:
- 用工具(比如TexturePacker)把所有纹理合并成一张大图,记录每个纹理在图集中的UV偏移和缩放比例。
- 在tinygltf中,为每个顶点组调整
TEXCOORD_0的UV值,对应到图集中的位置。 - 整个网格只需要一个Primitive和一个材质(绑定图集纹理)。
这种方案的优势是Draw Call最少,性能最优;缺点是需要预处理纹理和UV,灵活性较低(比如纹理尺寸变化时需要重新生成图集)。
总结选择建议
- 如果你的纹理分组清晰、数量不多:优先选方案1,实现简单且兼容性好。
- 如果是复杂网格、追求低Draw Call:选方案2,但需要自定义着色器和纹理绑定逻辑。
- 如果是大量小纹理的场景:选方案3,性能最优但需要预处理。
内容的提问来源于stack exchange,提问作者Fezzan Mahmood

