Chenyj Space
返回博客
·32 分钟

3D 网格体(Mesh)完全指南:从基础到高级

图形学Mesh3D建模几何处理tech

什么是网格体(Mesh)?

网格体(Mesh)是 3D 计算机图形学中最基本的几何表示形式。它由一系列顶点(Vertices)、边(Edges)和面(Faces)组成,用来描述 3D 物体的形状和表面。

网格体是实时渲染、3D 建模、游戏开发和科学可视化等领域的核心数据结构。

网格体的基本组成

1. 顶点(Vertex)

顶点是网格体的最基本元素,表示 3D 空间中的一个点:

cpp
struct Vertex {
    glm::vec3 position;  // 位置坐标
    glm::vec3 normal;    // 法线向量
    glm::vec2 texCoord;  // 纹理坐标
    glm::vec3 tangent;   // 切线向量
    glm::vec3 color;     // 顶点颜色
};

2. 边(Edge)

边连接两个顶点,是三角形的边界:

cpp
struct Edge {
    uint32_t vertex0;
    uint32_t vertex1;
    uint32_t face0;
    uint32_t face1;
};

3. 面(Face)

面通常由三个顶点组成的三角形(Triangle):

cpp
struct Triangle {
    uint32_t indices[3];  // 三个顶点的索引
};

网格体的数据结构

1. 三角形列表(Triangle List)

最简单的表示方式,每三个顶点组成一个三角形:

cpp
std::vector<Vertex> vertices = {
    // 三角形 1
    {glm::vec3(-1, 0, 0), glm::vec3(0, 1, 0)},
    {glm::vec3(1, 0, 0), glm::vec3(0, 1, 0)},
    {glm::vec3(0, 1, 0), glm::vec3(0, 1, 0)},
    // 三角形 2
    // ...
};

2. 索引三角形列表(Indexed Triangle List)

使用顶点索引来避免重复顶点:

cpp
std::vector<Vertex> vertices = {
    {glm::vec3(-1, 0, 0)},  // 0
    {glm::vec3(1, 0, 0)},   // 1
    {glm::vec3(0, 1, 0)},   // 2
    {glm::vec3(0, -1, 0)},  // 3
};

std::vector<uint32_t> indices = {
    0, 1, 2,  // 三角形 1
    0, 1, 3,  // 三角形 2
};

3. 半边结构(Half-Edge)

高级数据结构,便于遍历和修改:

cpp
struct HalfEdge {
    uint32_t vertex;
    uint32_t face;
    uint32_t next;
    uint32_t twin;
};

struct Mesh {
    std::vector<glm::vec3> positions;
    std::vector<HalfEdge> halfEdges;
    std::vector<uint32_t> faces;
};

网格体的拓扑

1. 流形(Manifold)

流形网格满足以下条件:

  • 每条边恰好被两个面共享
  • 每个顶点周围的面形成一个闭合的环
cpp
bool isManifold(const Mesh& mesh) {
    // 检查每条边是否被恰好两个面共享
    std::unordered_map<EdgeKey, int> edgeCount;
    
    for (const auto& triangle : mesh.triangles) {
        for (int i = 0; i < 3; i++) {
            EdgeKey key = makeEdgeKey(triangle.indices[i], triangle.indices[(i + 1) % 3]);
            edgeCount[key]++;
        }
    }
    
    for (const auto& [key, count] : edgeCount) {
        if (count != 2) {
            return false;
        }
    }
    
    return true;
}

2. 亏格(Genus)

亏格描述网格体的"洞"的数量:

  • 亏格 0:球体、立方体
  • 亏格 1:环面(甜甜圈)
  • 亏格 2:双环面
cpp
int computeGenus(const Mesh& mesh) {
    int V = mesh.vertices.size();
    int E = mesh.edges.size();
    int F = mesh.triangles.size();
    
    // 欧拉公式:V - E + F = 2 - 2g
    return (2 - V + E - F) / 2;
}

网格体的几何属性

1. 法线计算

顶点法线可以通过相邻面的法线平均得到:

cpp
glm::vec3 computeFaceNormal(const glm::vec3& v0, const glm::vec3& v1, const glm::vec3& v2) {
    glm::vec3 edge1 = v1 - v0;
    glm::vec3 edge2 = v2 - v0;
    return glm::normalize(glm::cross(edge1, edge2));
}

void computeVertexNormals(Mesh& mesh) {
    mesh.normals.resize(mesh.vertices.size(), glm::vec3(0.0f));
    
    for (const auto& tri : mesh.triangles) {
        glm::vec3 normal = computeFaceNormal(
            mesh.vertices[tri.indices[0]],
            mesh.vertices[tri.indices[1]],
            mesh.vertices[tri.indices[2]]
        );
        
        mesh.normals[tri.indices[0]] += normal;
        mesh.normals[tri.indices[1]] += normal;
        mesh.normals[tri.indices[2]] += normal;
    }
    
    for (auto& normal : mesh.normals) {
        normal = glm::normalize(normal);
    }
}

2. 面积计算

三角形面积可以通过叉积计算:

cpp
float computeTriangleArea(const glm::vec3& v0, const glm::vec3& v1, const glm::vec3& v2) {
    glm::vec3 edge1 = v1 - v0;
    glm::vec3 edge2 = v2 - v0;
    return 0.5f * glm::length(glm::cross(edge1, edge2));
}

float computeMeshArea(const Mesh& mesh) {
    float area = 0.0f;
    
    for (const auto& tri : mesh.triangles) {
        area += computeTriangleArea(
            mesh.vertices[tri.indices[0]],
            mesh.vertices[tri.indices[1]],
            mesh.vertices[tri.indices[2]]
        );
    }
    
    return area;
}

3. 包围盒

轴对齐包围盒(AABB)用于快速剔除:

cpp
struct AABB {
    glm::vec3 min;
    glm::vec3 max;
};

AABB computeAABB(const Mesh& mesh) {
    AABB bbox;
    bbox.min = glm::vec3(FLT_MAX);
    bbox.max = glm::vec3(-FLT_MAX);
    
    for (const auto& vertex : mesh.vertices) {
        bbox.min = glm::min(bbox.min, vertex);
        bbox.max = glm::max(bbox.max, vertex);
    }
    
    return bbox;
}

网格体优化

1. 网格简化

减少三角形数量,保持形状:

cpp
// 边折叠(Edge Collapse)简化
void simplifyMesh(Mesh& mesh, int targetTriangles) {
    while (mesh.triangles.size() > targetTriangles) {
        // 找到代价最小的边
        Edge minEdge = findMinCostEdge(mesh);
        
        // 执行边折叠
        collapseEdge(mesh, minEdge);
    }
}

2. 网格细分

增加细节,平滑表面:

cpp
// Loop 细分
void subdivideLoop(Mesh& mesh) {
    std::vector<Vertex> newVertices;
    std::vector<Triangle> newTriangles;
    
    // 为每条边创建新顶点
    for (const auto& edge : mesh.edges) {
        glm::vec3 newPos = computeEdgePosition(mesh, edge);
        newVertices.push_back({newPos});
    }
    
    // 更新原始顶点位置
    for (size_t i = 0; i < mesh.vertices.size(); i++) {
        mesh.vertices[i].position = computeVertexPosition(mesh, i);
    }
    
    // 创建新三角形
    for (const auto& tri : mesh.triangles) {
        // 每个三角形分成四个三角形
        // ...
    }
}

3. 网格平滑

消除噪声,平滑表面:

cpp
// 拉普拉斯平滑
void smoothMesh(Mesh& mesh, int iterations, float lambda) {
    for (int iter = 0; iter < iterations; iter++) {
        std::vector<glm::vec3> newPositions(mesh.vertices.size());
        
        for (size_t i = 0; i < mesh.vertices.size(); i++) {
            glm::vec3 laplacian(0.0f);
            int count = 0;
            
            // 计算相邻顶点的平均位置
            for (uint32_t neighbor : getNeighbors(mesh, i)) {
                laplacian += mesh.vertices[neighbor].position;
                count++;
            }
            
            laplacian /= static_cast<float>(count);
            laplacian -= mesh.vertices[i].position;
            
            newPositions[i] = mesh.vertices[i].position + lambda * laplacian;
        }
        
        // 更新顶点位置
        for (size_t i = 0; i < mesh.vertices.size(); i++) {
            mesh.vertices[i].position = newPositions[i];
        }
    }
}

高级网格处理

1. 网格分割

将大网格分成多个小块:

cpp
std::vector<Mesh> splitMesh(const Mesh& mesh, int maxTrianglesPerChunk) {
    std::vector<Mesh> chunks;
    
    // 使用空间分割或图分割算法
    // ...
    
    return chunks;
}

2. Meshlet 生成

为 GPU 渲染优化的小块:

cpp
struct Meshlet {
    uint32_t vertexCount;
    uint32_t triangleCount;
    uint32_t vertices[64];
    uint8_t indices[124 * 3];
};

std::vector<Meshlet> generateMeshlets(const Mesh& mesh) {
    // 使用 meshoptimizer 库
    size_t maxMeshlets = meshopt_buildMeshletsBound(
        mesh.indices.size(), 64, 124);
    
    std::vector<meshopt_Meshlet> meshlets(maxMeshlets);
    std::vector<unsigned int> meshletVertices(maxMeshlets * 64);
    std::vector<unsigned char> meshletTriangles(maxMeshlets * 124 * 3);
    
    size_t meshletCount = meshopt_buildMeshlets(
        meshlets.data(), meshletVertices.data(), meshletTriangles.data(),
        mesh.indices.data(), mesh.indices.size(),
        &mesh.vertices[0].position.x, mesh.vertices.size(), sizeof(Vertex),
        64, 124, 0.0f);
    
    // 转换为自定义格式
    // ...
}

3. 碰撞检测

使用网格进行物理碰撞:

cpp
bool rayMeshIntersection(const Ray& ray, const Mesh& mesh, float& t) {
    bool hit = false;
    t = FLT_MAX;
    
    for (const auto& tri : mesh.triangles) {
        float tTri;
        if (rayTriangleIntersection(ray,
            mesh.vertices[tri.indices[0]].position,
            mesh.vertices[tri.indices[1]].position,
            mesh.vertices[tri.indices[2]].position,
            tTri)) {
            if (tTri < t) {
                t = tTri;
                hit = true;
            }
        }
    }
    
    return hit;
}

网格文件格式

1. OBJ 格式

最常见的 3D 模型格式:

obj
# 顶点
v 1.0 0.0 0.0
v 0.0 1.0 0.0
v 0.0 0.0 1.0

# 法线
vn 0.0 0.0 1.0

# 纹理坐标
vt 0.0 0.0
vt 1.0 0.0
vt 0.5 1.0

# 面(顶点/纹理/法线)
f 1/1/1 2/2/1 3/3/1

2. glTF 格式

现代的 3D 传输格式:

json
{
  "meshes": [{
    "primitives": [{
      "attributes": {
        "POSITION": 0,
        "NORMAL": 1,
        "TEXCOORD_0": 2
      },
      "indices": 3
    }]
  }]
}

3. FBX 格式

工业标准格式,支持动画:

cpp
// 使用 FBX SDK 加载
FbxManager* manager = FbxManager::Create();
FbxScene* scene = FbxScene::create(manager, "");
FbxImporter* importer = FbxImporter::create(manager, "");

importer->Initialize("model.fbx");
importer->Import(scene);

// 遍历网格节点
FbxNode* root = scene->GetRootNode();
for (int i = 0; i < root->GetChildCount(); i++) {
    FbxNode* node = root->GetChild(i);
    FbxMesh* mesh = node->GetMesh();
    
    if (mesh) {
        // 提取顶点和索引
        // ...
    }
}

性能优化

1. 顶点缓存优化

重新排序顶点以提高缓存命中率:

cpp
void optimizeVertexCache(Mesh& mesh) {
    // 使用 Forsyth 算法或类似方法
    meshopt_optimizeVertexCache(
        mesh.indices.data(), mesh.indices.data(),
        mesh.indices.size(), mesh.vertices.size());
}

2. 顶点预取优化

优化顶点数据布局:

cpp
void optimizeVertexFetch(Mesh& mesh) {
    meshopt_optimizeVertexFetch(
        mesh.vertices.data(), mesh.indices.data(),
        mesh.indices.size(), mesh.vertices.data(),
        mesh.vertices.size(), sizeof(Vertex));
}

3. Overdraw 优化

减少过度绘制:

cpp
void optimizeOverdraw(Mesh& mesh) {
    meshopt_optimizeOverdraw(
        mesh.indices.data(), mesh.indices.data(),
        mesh.indices.size(), &mesh.vertices[0].position.x,
        mesh.vertices.size(), sizeof(Vertex), 1.0f);
}

常用工具

1. meshoptimizer

高性能网格优化库:

cpp
#include <meshoptimizer.h>

// 简化
meshopt_simplify(indices, indices, index_count, vertices, vertex_count,
                 sizeof(Vertex), target_index_count, target_error);

2. Assimp

模型导入库:

cpp
#include <assimp/Importer.hpp>
#include <assimp/scene.h>
#include <assimp/postprocess.h>

Assimp::Importer importer;
const aiScene* scene = importer.ReadFile("model.obj",
    aiProcess_Triangulate | aiProcess_GenNormals);

3. OpenMesh

半边网格库:

cpp
#include <OpenMesh/Core/IO/MeshIO.hh>
#include <OpenMesh/Core/Mesh/TriMesh_ArrayKernelT.hh>

typedef OpenMesh::TriMesh_ArrayKernelT<> MyMesh;
MyMesh mesh;

OpenMesh::IO::read_mesh(mesh, "model.obj");

实际应用

1. 游戏开发

  • 角色模型
  • 场景环境
  • 碰撞体

2. 3D 打印

  • 模型修复
  • 支撑生成
  • 切片处理

3. 科学可视化

  • 有限元网格
  • 医学图像重建
  • 地形生成

总结

网格体是 3D 图形学的基础,理解其核心概念对于图形编程至关重要:

  1. 基本组成:顶点、边、面
  2. 数据结构:索引列表、半边结构
  3. 几何属性:法线、面积、包围盒
  4. 优化技术:简化、细分、平滑
  5. 文件格式:OBJ、glTF、FBX

参考资源