如何编写OBJ解析器实现为每个顶点匹配对应法向量的功能
实现方案
核心问题
OBJ格式的面(f)索引是三组独立的索引,分别对应顶点位置、纹理坐标、法向量,三者没有绑定关系。你当前的代码只提取了顶点位置索引,直接用该索引去法向量数组取值,自然会出现数量不匹配、对应关系错误的问题。
具体实现步骤
- 首先完善面解析逻辑:修改
ParseFace函数的返回结构,除了顶点位置索引VertexIndices,新增纹理坐标索引TexIndices、法向量索引NormalIndices,把f v/vt/vn格式里的三组索引全部解析出来,注意OBJ索引默认从1开始,解析后要减1适配C++数组从0开始的规则。 - 定义渲染顶点结构:将「位置索引+纹理索引+法向量索引」三个值打包为一个Key,用来判断是否是同一个可渲染顶点,也可以直接把三个属性值打包作为Key。
- 构建索引映射表:用
std::map或者std::unordered_map存储Key到最终顶点索引的映射,遍历所有面的所有顶点组合:- 如果当前Key不存在于映射表中:新建一个顶点,从已缓存的位置数组、UV数组、法向量数组中分别取出对应索引的属性,存入Mesh的最终顶点数组、UV数组、法向量数组,将新生成的索引存入映射表,同时把该索引加入Mesh的Indices数组。
- 如果当前Key已经存在:直接取映射表中对应的索引加入Indices数组即可。
- 边界适配:如果解析的OBJ文件没有法向量/UV数据,可以选择自动计算面法向量赋值给对应顶点,或者默认填充默认值即可。
代码修改示例
// 先定义用来做去重的Key结构 struct VertexKey { int v_idx; int vt_idx; int vn_idx; // 重载==用于比较 bool operator==(const VertexKey& other) const { return v_idx == other.v_idx && vt_idx == other.vt_idx && vn_idx == other.vn_idx; } }; // 给Key定义哈希函数,用于unordered_map namespace std { template<> struct hash<VertexKey> { size_t operator()(const VertexKey& k) const { return ((hash<int>()(k.v_idx) ^ (hash<int>()(k.vt_idx) << 1)) >> 1) ^ (hash<int>()(k.vn_idx) << 1); } }; } // 修改后的LoadMeshData逻辑 void LoadMeshData(mesh_t* Mesh, char* FileName) { FILE* file = NULL; fopen_s(&file, FileName, "r"); // 临时存储从OBJ读取的原始属性数组,不要直接存入Mesh std::vector<v3> temp_vertices; std::vector<tex2d> temp_uvs; std::vector<v3> temp_normals; std::unordered_map<VertexKey, uint32_t> key_to_idx; char Line[256]; while(fgets(Line, 256, (FILE*)file) != NULL) { int32_t CharIdx = 0; char PrevChar = 0; char Char = Line[CharIdx]; while(Char) { Char = Line[CharIdx++]; if(Char == '#' || Char == 'm' || Char == 'o' && Char == 'u' || Char == 's') break; if((PrevChar == 'v' && Char == 't')) { v3 Coords = ParseVertex(Line); tex2d UVCoords = V3ToUVs(Coords); temp_uvs.push_back(UVCoords); break; } if((PrevChar == 'v' && Char == 'n')) { v3 Normals = ParseVertex(Line); temp_normals.push_back(Normals); break; } if((PrevChar == 'v' && Char == ' ')) { v3 Vert = ParseVertex(Line); temp_vertices.push_back(Vert); break; } if((PrevChar == 'f' && Char == ' ')) { parsed_obj Obj = ParseFace(Line); // 需提前修改ParseFace返回三组索引 // 遍历面的三个顶点 for(int i=0; i<3; i++) { VertexKey key = { Obj.VertexIndices[i]-1, Obj.TexIndices[i]-1, Obj.NormalIndices[i]-1 }; // 不存在则新增顶点 if(key_to_idx.find(key) == key_to_idx.end()) { Mesh->Vertices.push_back(temp_vertices[key.v_idx]); Mesh->TextureCoords.push_back(temp_uvs[key.vt_idx]); Mesh->Normals.push_back(temp_normals[key.vn_idx]); key_to_idx[key] = Mesh->Vertices.size() - 1; } // 添加索引 Mesh->Indices.push_back(key_to_idx[key]); } break; } PrevChar = Char; } } fclose(file); }
按照上述逻辑处理后,Mesh的Vertices、Normals、TextureCoords数组长度会完全一致,每个索引值对应的三个属性完全匹配,即可实现每个顶点对应唯一法向量的需求。
内容的提问来源于stack exchange,提问作者Zhukov Artem
相关产品推荐
相关产品推荐

