Skip to content

第7章:几何建模基础

7.1 几何表示

7.1.1 几何表示的分类

在计算机图形学中,3D 几何体有多种表示方法,每种方法有其优缺点和适用场景。

几何表示方法分类

┌────────────────────────────────────────────────────────────┐
│                      几何表示                               │
├────────────────────────────────────────────────────────────┤
│                                                            │
│  ┌──────────────────┐    ┌──────────────────┐             │
│  │    边界表示       │    │    体积表示       │             │
│  │  (B-Rep)         │    │  (Volume)        │             │
│  ├──────────────────┤    ├──────────────────┤             │
│  │ • 多边形网格     │    │ • 体素           │             │
│  │ • 曲面           │    │ • CSG            │             │
│  │ • 细分曲面       │    │ • 隐式曲面       │             │
│  └──────────────────┘    └──────────────────┘             │
│                                                            │
│  ┌──────────────────┐    ┌──────────────────┐             │
│  │    参数表示       │    │    过程表示       │             │
│  │  (Parametric)    │    │  (Procedural)    │             │
│  ├──────────────────┤    ├──────────────────┤             │
│  │ • 贝塞尔曲面     │    │ • 分形           │             │
│  │ • NURBS          │    │ • L-系统         │             │
│  │ • B-样条曲面     │    │ • 噪声           │             │
│  └──────────────────┘    └──────────────────┘             │
│                                                            │
└────────────────────────────────────────────────────────────┘

7.1.2 各种表示的对比

表示方法优点缺点应用场景
多边形网格GPU 友好、简单不精确游戏、实时渲染
NURBS精确、光滑复杂CAD/CAM
细分曲面任意拓扑、光滑计算量大动画、电影
体素布尔运算简单内存大医学成像
CSG精确、便于编辑渲染复杂CAD
隐式曲面拓扑变化自然渲染困难特效

7.2 多边形网格

7.2.1 网格的基本概念

多边形网格的组成

多边形网格由三种基本元素组成:

1. 顶点(Vertex):3D 空间中的点
2. 边(Edge):连接两个顶点的线段
3. 面(Face):由边围成的多边形


三角形网格示例:

       V2
       /\
      /  \
     /    \
    /  F1  \
   /        \
  V0────────V1

顶点:V0, V1, V2
边:  E0(V0-V1), E1(V1-V2), E2(V2-V0)
面:  F1(V0, V1, V2)


欧拉公式:
V - E + F = 2(对于简单多面体)

例如:立方体
V = 8, E = 12, F = 6
8 - 12 + 6 = 2 ✓

7.2.2 网格数据

网格包含的数据

基本几何数据:
- 顶点位置 (x, y, z)

附加属性(per-vertex):
- 法向量 (nx, ny, nz)
- 纹理坐标 (u, v)
- 顶点颜色 (r, g, b, a)
- 骨骼权重(动画用)

拓扑信息:
- 顶点索引(定义面)
- 邻接关系(可选)


示例数据结构:

vertices = [
    { position: [0, 0, 0], normal: [0, 1, 0], uv: [0, 0] },
    { position: [1, 0, 0], normal: [0, 1, 0], uv: [1, 0] },
    { position: [1, 0, 1], normal: [0, 1, 0], uv: [1, 1] },
    { position: [0, 0, 1], normal: [0, 1, 0], uv: [0, 1] }
]

indices = [
    0, 1, 2,  // 第一个三角形
    0, 2, 3   // 第二个三角形
]

7.2.3 JavaScript 网格实现

javascript
/**
 * 三角形网格类
 */
class TriangleMesh {
    constructor() {
        this.vertices = [];     // 顶点位置数组
        this.normals = [];      // 法向量数组
        this.uvs = [];          // 纹理坐标数组
        this.indices = [];      // 索引数组
    }
    
    /**
     * 添加顶点
     */
    addVertex(position, normal = null, uv = null) {
        const index = this.vertices.length;
        this.vertices.push({ ...position });
        
        if (normal) this.normals.push({ ...normal });
        if (uv) this.uvs.push({ ...uv });
        
        return index;
    }
    
    /**
     * 添加三角形(通过索引)
     */
    addTriangle(i0, i1, i2) {
        this.indices.push(i0, i1, i2);
    }
    
    /**
     * 计算面法向量
     */
    computeFaceNormal(i0, i1, i2) {
        const v0 = this.vertices[i0];
        const v1 = this.vertices[i1];
        const v2 = this.vertices[i2];
        
        // 边向量
        const e1 = {
            x: v1.x - v0.x,
            y: v1.y - v0.y,
            z: v1.z - v0.z
        };
        const e2 = {
            x: v2.x - v0.x,
            y: v2.y - v0.y,
            z: v2.z - v0.z
        };
        
        // 叉积
        const normal = {
            x: e1.y * e2.z - e1.z * e2.y,
            y: e1.z * e2.x - e1.x * e2.z,
            z: e1.x * e2.y - e1.y * e2.x
        };
        
        // 归一化
        const len = Math.sqrt(
            normal.x * normal.x + 
            normal.y * normal.y + 
            normal.z * normal.z
        );
        
        if (len > 0) {
            normal.x /= len;
            normal.y /= len;
            normal.z /= len;
        }
        
        return normal;
    }
    
    /**
     * 计算所有顶点的法向量(平滑着色)
     */
    computeVertexNormals() {
        // 初始化法向量
        this.normals = this.vertices.map(() => ({ x: 0, y: 0, z: 0 }));
        
        // 累加每个面对顶点法向量的贡献
        for (let i = 0; i < this.indices.length; i += 3) {
            const i0 = this.indices[i];
            const i1 = this.indices[i + 1];
            const i2 = this.indices[i + 2];
            
            const faceNormal = this.computeFaceNormal(i0, i1, i2);
            
            // 累加到顶点
            for (const idx of [i0, i1, i2]) {
                this.normals[idx].x += faceNormal.x;
                this.normals[idx].y += faceNormal.y;
                this.normals[idx].z += faceNormal.z;
            }
        }
        
        // 归一化
        for (const normal of this.normals) {
            const len = Math.sqrt(
                normal.x * normal.x + 
                normal.y * normal.y + 
                normal.z * normal.z
            );
            if (len > 0) {
                normal.x /= len;
                normal.y /= len;
                normal.z /= len;
            }
        }
    }
    
    /**
     * 计算包围盒
     */
    computeBoundingBox() {
        let min = { x: Infinity, y: Infinity, z: Infinity };
        let max = { x: -Infinity, y: -Infinity, z: -Infinity };
        
        for (const v of this.vertices) {
            min.x = Math.min(min.x, v.x);
            min.y = Math.min(min.y, v.y);
            min.z = Math.min(min.z, v.z);
            max.x = Math.max(max.x, v.x);
            max.y = Math.max(max.y, v.y);
            max.z = Math.max(max.z, v.z);
        }
        
        return { min, max };
    }
    
    /**
     * 计算表面积
     */
    computeSurfaceArea() {
        let area = 0;
        
        for (let i = 0; i < this.indices.length; i += 3) {
            const v0 = this.vertices[this.indices[i]];
            const v1 = this.vertices[this.indices[i + 1]];
            const v2 = this.vertices[this.indices[i + 2]];
            
            // 三角形面积 = |AB × AC| / 2
            const e1 = { x: v1.x - v0.x, y: v1.y - v0.y, z: v1.z - v0.z };
            const e2 = { x: v2.x - v0.x, y: v2.y - v0.y, z: v2.z - v0.z };
            
            const cross = {
                x: e1.y * e2.z - e1.z * e2.y,
                y: e1.z * e2.x - e1.x * e2.z,
                z: e1.x * e2.y - e1.y * e2.x
            };
            
            area += Math.sqrt(
                cross.x * cross.x + 
                cross.y * cross.y + 
                cross.z * cross.z
            ) / 2;
        }
        
        return area;
    }
    
    /**
     * 转换为 WebGL 缓冲区格式
     */
    toBuffers() {
        const positions = new Float32Array(this.vertices.length * 3);
        const normals = this.normals.length > 0 
            ? new Float32Array(this.normals.length * 3) 
            : null;
        const uvs = this.uvs.length > 0 
            ? new Float32Array(this.uvs.length * 2) 
            : null;
        const indices = new Uint16Array(this.indices);
        
        for (let i = 0; i < this.vertices.length; i++) {
            positions[i * 3] = this.vertices[i].x;
            positions[i * 3 + 1] = this.vertices[i].y;
            positions[i * 3 + 2] = this.vertices[i].z;
            
            if (normals && this.normals[i]) {
                normals[i * 3] = this.normals[i].x;
                normals[i * 3 + 1] = this.normals[i].y;
                normals[i * 3 + 2] = this.normals[i].z;
            }
            
            if (uvs && this.uvs[i]) {
                uvs[i * 2] = this.uvs[i].u;
                uvs[i * 2 + 1] = this.uvs[i].v;
            }
        }
        
        return { positions, normals, uvs, indices };
    }
}

7.3 网格数据结构

7.3.1 索引面集

索引面集(Indexed Face Set)

最简单的网格表示,分离存储顶点和面:

顶点数组:
V0: (0, 0, 0)
V1: (1, 0, 0)
V2: (1, 1, 0)
V3: (0, 1, 0)

面数组(索引):
F0: [0, 1, 2]
F1: [0, 2, 3]


优点:
- 简单、紧凑
- GPU 友好

缺点:
- 查询邻接关系需要遍历
- 不便于局部编辑

7.3.2 半边结构

半边数据结构(Half-Edge)

每条边存储为两个相反方向的"半边":

        V2
        /\
       /  \
    h3/    \h2
     /  F0  \
    /        \
   V0───h0───►V1
       ◄──h1──
      \  F1  /
       \    /
      h4\  /h5
         \/
        V3

每个半边存储:
- 起点(vertex)
- 所属面(face)
- 下一条半边(next)
- 相反半边(twin/opposite)


半边的遍历:

绕顶点遍历所有邻接面:
从任意出射半边开始,不断取 twin->next

绕面遍历所有顶点:
从任意半边开始,不断取 next

7.3.3 半边结构实现

javascript
/**
 * 半边
 */
class HalfEdge {
    constructor() {
        this.vertex = null;     // 终点顶点
        this.face = null;       // 所属面
        this.next = null;       // 下一条半边(同一面内)
        this.prev = null;       // 上一条半边
        this.twin = null;       // 对偶半边
    }
}

/**
 * 顶点
 */
class HEVertex {
    constructor(x, y, z) {
        this.x = x;
        this.y = y;
        this.z = z;
        this.halfEdge = null;   // 一条出射半边
    }
}

/**
 * 面
 */
class HEFace {
    constructor() {
        this.halfEdge = null;   // 面上的一条半边
    }
    
    /**
     * 获取面的所有顶点
     */
    vertices() {
        const result = [];
        let he = this.halfEdge;
        do {
            result.push(he.vertex);
            he = he.next;
        } while (he !== this.halfEdge);
        return result;
    }
}

/**
 * 半边网格
 */
class HalfEdgeMesh {
    constructor() {
        this.vertices = [];
        this.faces = [];
        this.halfEdges = [];
    }
    
    /**
     * 从索引网格创建半边结构
     */
    static fromIndexedMesh(positions, indices) {
        const mesh = new HalfEdgeMesh();
        
        // 创建顶点
        for (let i = 0; i < positions.length; i += 3) {
            const v = new HEVertex(
                positions[i],
                positions[i + 1],
                positions[i + 2]
            );
            mesh.vertices.push(v);
        }
        
        // 用于查找对偶半边
        const edgeMap = new Map();
        
        function edgeKey(v1, v2) {
            return `${Math.min(v1, v2)}_${Math.max(v1, v2)}`;
        }
        
        // 创建面和半边
        for (let i = 0; i < indices.length; i += 3) {
            const face = new HEFace();
            mesh.faces.push(face);
            
            const faceIndices = [indices[i], indices[i + 1], indices[i + 2]];
            const faceHalfEdges = [];
            
            // 创建三条半边
            for (let j = 0; j < 3; j++) {
                const he = new HalfEdge();
                he.vertex = mesh.vertices[faceIndices[(j + 1) % 3]];
                he.face = face;
                mesh.halfEdges.push(he);
                faceHalfEdges.push(he);
                
                // 设置顶点的出射半边
                if (!mesh.vertices[faceIndices[j]].halfEdge) {
                    mesh.vertices[faceIndices[j]].halfEdge = he;
                }
            }
            
            // 设置 next 和 prev
            for (let j = 0; j < 3; j++) {
                faceHalfEdges[j].next = faceHalfEdges[(j + 1) % 3];
                faceHalfEdges[j].prev = faceHalfEdges[(j + 2) % 3];
            }
            
            face.halfEdge = faceHalfEdges[0];
            
            // 查找并设置对偶半边
            for (let j = 0; j < 3; j++) {
                const v1 = faceIndices[j];
                const v2 = faceIndices[(j + 1) % 3];
                const key = edgeKey(v1, v2);
                
                if (edgeMap.has(key)) {
                    const twin = edgeMap.get(key);
                    faceHalfEdges[j].twin = twin;
                    twin.twin = faceHalfEdges[j];
                } else {
                    edgeMap.set(key, faceHalfEdges[j]);
                }
            }
        }
        
        return mesh;
    }
    
    /**
     * 获取顶点的所有邻接面
     */
    getVertexFaces(vertex) {
        const faces = [];
        let he = vertex.halfEdge;
        const start = he;
        
        do {
            if (he.face) faces.push(he.face);
            he = he.twin?.next;
        } while (he && he !== start);
        
        return faces;
    }
    
    /**
     * 获取顶点的所有邻接顶点
     */
    getVertexNeighbors(vertex) {
        const neighbors = [];
        let he = vertex.halfEdge;
        const start = he;
        
        do {
            neighbors.push(he.vertex);
            he = he.twin?.next;
        } while (he && he !== start);
        
        return neighbors;
    }
    
    /**
     * 获取边的两个邻接面
     */
    getEdgeFaces(halfEdge) {
        return [halfEdge.face, halfEdge.twin?.face].filter(f => f);
    }
    
    /**
     * 边翻转(用于网格优化)
     */
    flipEdge(halfEdge) {
        if (!halfEdge.twin) return false; // 边界边
        
        const he = halfEdge;
        const twin = he.twin;
        
        // 获取四个顶点
        const a = he.next.vertex;
        const b = twin.next.vertex;
        const c = he.vertex;
        const d = twin.vertex;
        
        // 更新半边连接
        // ... (复杂的指针操作)
        
        return true;
    }
}

7.4 细分曲面

7.4.1 细分曲面的概念

细分曲面(Subdivision Surface)

通过递归细分低分辨率网格,生成光滑曲面。

          初始                 一次细分              多次细分
       ┌────────┐           ┌──┬──┬──┐          更加光滑...
       │        │           ├──┼──┼──┤
       │        │   ──►     ├──┼──┼──┤    ──►
       │        │           ├──┼──┼──┤
       └────────┘           └──┴──┴──┘


常见的细分方案:

1. Catmull-Clark(四边形网格)
   - 应用最广泛
   - 极限曲面是 C² 连续的双三次 B-样条

2. Loop(三角形网格)
   - 专用于三角形
   - 极限曲面是 C² 连续的

3. Doo-Sabin
   - 较早的方案
   - 双二次 B-样条

7.4.2 Catmull-Clark 细分

Catmull-Clark 细分规则

步骤:

1. 面点(Face Point):面的中心点
   F = 面所有顶点的平均

2. 边点(Edge Point):
   E = (V1 + V2 + F1 + F2) / 4
   其中 V1, V2 是边端点,F1, F2 是邻接面点

3. 新顶点位置(Vertex Point):
   V' = (Q + 2R + (n-3)V) / n
   其中:
   - Q = 邻接面点平均
   - R = 邻接边中点平均
   - n = 顶点的度(邻接边数)


图示:

原始网格:              细分后:

     V1────────V2            V1──E1──F──E2──V2
      │        │              │   │   │   │
      │   F    │      ──►     E4──●───●───E5
      │        │              │   │   │   │
     V4────────V3            V4──E3──F──E6──V3

F: 面点
E1-E6: 边点
V1'-V4': 新顶点位置

7.4.3 Catmull-Clark 实现

javascript
/**
 * Catmull-Clark 细分
 */
class CatmullClarkSubdivision {
    /**
     * 执行一次细分
     * @param mesh 输入网格(四边形面)
     * @returns 细分后的网格
     */
    static subdivide(mesh) {
        const newMesh = new TriangleMesh();
        
        // 1. 计算面点
        const facePoints = [];
        for (const face of mesh.faces) {
            const vertices = face.vertices();
            const fp = {
                x: vertices.reduce((s, v) => s + v.x, 0) / vertices.length,
                y: vertices.reduce((s, v) => s + v.y, 0) / vertices.length,
                z: vertices.reduce((s, v) => s + v.z, 0) / vertices.length
            };
            facePoints.push(fp);
        }
        
        // 2. 计算边点
        const edgePoints = new Map();
        
        for (const he of mesh.halfEdges) {
            if (edgePoints.has(he) || (he.twin && edgePoints.has(he.twin))) {
                continue;
            }
            
            const v1 = he.prev.vertex;
            const v2 = he.vertex;
            
            let ep;
            if (he.twin) {
                // 内部边:使用面点
                const f1 = mesh.faces.indexOf(he.face);
                const f2 = mesh.faces.indexOf(he.twin.face);
                const fp1 = facePoints[f1];
                const fp2 = facePoints[f2];
                
                ep = {
                    x: (v1.x + v2.x + fp1.x + fp2.x) / 4,
                    y: (v1.y + v2.y + fp1.y + fp2.y) / 4,
                    z: (v1.z + v2.z + fp1.z + fp2.z) / 4
                };
            } else {
                // 边界边:简单中点
                ep = {
                    x: (v1.x + v2.x) / 2,
                    y: (v1.y + v2.y) / 2,
                    z: (v1.z + v2.z) / 2
                };
            }
            
            edgePoints.set(he, ep);
            if (he.twin) edgePoints.set(he.twin, ep);
        }
        
        // 3. 计算新顶点位置
        const vertexPoints = [];
        
        for (let i = 0; i < mesh.vertices.length; i++) {
            const v = mesh.vertices[i];
            
            // 获取邻接信息
            const adjacentFaces = mesh.getVertexFaces(v);
            const adjacentEdges = [];
            let he = v.halfEdge;
            const start = he;
            do {
                adjacentEdges.push(he);
                he = he.twin?.next;
            } while (he && he !== start);
            
            const n = adjacentEdges.length;
            
            if (n < 3) {
                // 边界顶点:保持位置或使用边界规则
                vertexPoints.push({ ...v });
                continue;
            }
            
            // Q: 邻接面点平均
            const Q = { x: 0, y: 0, z: 0 };
            for (const face of adjacentFaces) {
                const fi = mesh.faces.indexOf(face);
                Q.x += facePoints[fi].x;
                Q.y += facePoints[fi].y;
                Q.z += facePoints[fi].z;
            }
            Q.x /= adjacentFaces.length;
            Q.y /= adjacentFaces.length;
            Q.z /= adjacentFaces.length;
            
            // R: 邻接边中点平均
            const R = { x: 0, y: 0, z: 0 };
            for (const edge of adjacentEdges) {
                const mid = {
                    x: (v.x + edge.vertex.x) / 2,
                    y: (v.y + edge.vertex.y) / 2,
                    z: (v.z + edge.vertex.z) / 2
                };
                R.x += mid.x;
                R.y += mid.y;
                R.z += mid.z;
            }
            R.x /= n;
            R.y /= n;
            R.z /= n;
            
            // 新顶点位置
            const vp = {
                x: (Q.x + 2 * R.x + (n - 3) * v.x) / n,
                y: (Q.y + 2 * R.y + (n - 3) * v.y) / n,
                z: (Q.z + 2 * R.z + (n - 3) * v.z) / n
            };
            
            vertexPoints.push(vp);
        }
        
        // 4. 构建新网格
        // 每个原始面变成多个四边形
        // ... (创建新顶点和面的连接)
        
        return newMesh;
    }
}

/**
 * Loop 细分(三角形网格)
 */
class LoopSubdivision {
    /**
     * 执行一次细分
     */
    static subdivide(mesh) {
        const newMesh = new TriangleMesh();
        
        // 计算边点(每条边的中点,加权)
        const edgeVertices = new Map();
        
        // 计算新顶点位置
        const newPositions = [];
        
        for (let i = 0; i < mesh.vertices.length; i++) {
            const v = mesh.vertices[i];
            const neighbors = mesh.getVertexNeighbors(mesh.vertices[i]);
            const n = neighbors.length;
            
            // Loop 细分的 beta 系数
            let beta;
            if (n === 3) {
                beta = 3 / 16;
            } else {
                beta = 3 / (8 * n);
            }
            
            // 新位置 = (1 - n*beta) * v + beta * sum(neighbors)
            const sumNeighbors = { x: 0, y: 0, z: 0 };
            for (const neighbor of neighbors) {
                sumNeighbors.x += neighbor.x;
                sumNeighbors.y += neighbor.y;
                sumNeighbors.z += neighbor.z;
            }
            
            newPositions.push({
                x: (1 - n * beta) * v.x + beta * sumNeighbors.x,
                y: (1 - n * beta) * v.y + beta * sumNeighbors.y,
                z: (1 - n * beta) * v.z + beta * sumNeighbors.z
            });
        }
        
        // ... 继续构建细分网格
        
        return newMesh;
    }
}

7.5 网格简化

7.5.1 简化的目的

网格简化(Mesh Simplification)

目的:减少网格的面数,同时尽量保持外观。

高细节网格(10000 面):          简化后(1000 面):
    ╱╲╱╲╱╲╱╲                        ╱╲╱╲
   ╱╲╱╲╱╲╱╲╱╲                      ╱╲╱╲╱╲
  ╱╲╱╲╱╲╱╲╱╲╱╲         ──►        ╱╲╱╲╱╲╱╲
   ╲╱╲╱╲╱╲╱╲╱                      ╲╱╲╱╲╱
    ╲╱╲╱╲╱╲╱                        ╲╱╲╱


应用场景:
- LOD(Level of Detail)
- 实时渲染优化
- 网络传输
- 存储优化

7.5.2 边折叠算法

边折叠(Edge Collapse)

将一条边的两个端点合并为一个点。


    V1             V1              V1
   /|\            / \            / \
  / | \          /   \          /   \
 /  |  \   ──►  / V01 \   ──►  / V01 \
V2──●───V3     V2─────V3      V2─────V3
    V0

边 V0-V1 折叠,V0 和 V1 合并为 V01


简化策略:

1. 计算每条边的"代价"(误差度量)
2. 选择代价最小的边进行折叠
3. 更新受影响边的代价
4. 重复直到达到目标面数


常用误差度量:

1. 边长度(简单但效果差)
2. 二次误差度量(QEM)★
3. 曲率

7.5.3 QEM 简化实现

javascript
/**
 * 二次误差度量(QEM)网格简化
 */
class QEMSimplification {
    constructor(mesh) {
        this.mesh = mesh;
        this.quadrics = [];     // 每个顶点的 Q 矩阵
        this.edgeHeap = [];     // 边的优先队列
    }
    
    /**
     * 计算顶点的 Q 矩阵
     */
    computeVertexQuadric(vertexIndex) {
        const Q = this.createZeroMatrix4();
        const vertex = this.mesh.vertices[vertexIndex];
        const faces = this.mesh.getVertexFaces(vertex);
        
        for (const face of faces) {
            // 计算面的平面方程 ax + by + cz + d = 0
            const normal = this.mesh.computeFaceNormal(face);
            const d = -(
                normal.x * vertex.x + 
                normal.y * vertex.y + 
                normal.z * vertex.z
            );
            
            // Q += p * p^T(外积)
            const p = [normal.x, normal.y, normal.z, d];
            for (let i = 0; i < 4; i++) {
                for (let j = 0; j < 4; j++) {
                    Q[i][j] += p[i] * p[j];
                }
            }
        }
        
        return Q;
    }
    
    /**
     * 计算边折叠的代价
     */
    computeEdgeCost(v1Index, v2Index) {
        const Q1 = this.quadrics[v1Index];
        const Q2 = this.quadrics[v2Index];
        
        // Q_bar = Q1 + Q2
        const Q_bar = this.addMatrices(Q1, Q2);
        
        // 找最优收缩点(使误差最小的位置)
        // 简化版本:使用边中点
        const v1 = this.mesh.vertices[v1Index];
        const v2 = this.mesh.vertices[v2Index];
        
        const midpoint = {
            x: (v1.x + v2.x) / 2,
            y: (v1.y + v2.y) / 2,
            z: (v1.z + v2.z) / 2
        };
        
        // 计算误差 v^T * Q_bar * v
        const v = [midpoint.x, midpoint.y, midpoint.z, 1];
        let cost = 0;
        
        for (let i = 0; i < 4; i++) {
            for (let j = 0; j < 4; j++) {
                cost += v[i] * Q_bar[i][j] * v[j];
            }
        }
        
        return { cost, position: midpoint };
    }
    
    /**
     * 执行简化
     * @param targetFaceCount 目标面数
     */
    simplify(targetFaceCount) {
        // 初始化 Q 矩阵
        for (let i = 0; i < this.mesh.vertices.length; i++) {
            this.quadrics.push(this.computeVertexQuadric(i));
        }
        
        // 初始化边堆
        this.initializeEdgeHeap();
        
        // 迭代折叠边
        while (this.mesh.faces.length > targetFaceCount && 
               this.edgeHeap.length > 0) {
            // 取出代价最小的边
            const edge = this.popMinCostEdge();
            
            if (!this.isValidCollapse(edge)) {
                continue;
            }
            
            // 执行折叠
            this.collapseEdge(edge);
            
            // 更新受影响顶点的 Q 和边代价
            this.updateAffectedEdges(edge.newVertex);
        }
        
        return this.mesh;
    }
    
    /**
     * 检查折叠是否有效(防止网格翻转等问题)
     */
    isValidCollapse(edge) {
        // 检查拓扑有效性
        // 检查法向量翻转
        // ...
        return true;
    }
    
    // 辅助方法
    createZeroMatrix4() {
        return [[0,0,0,0], [0,0,0,0], [0,0,0,0], [0,0,0,0]];
    }
    
    addMatrices(A, B) {
        const result = this.createZeroMatrix4();
        for (let i = 0; i < 4; i++) {
            for (let j = 0; j < 4; j++) {
                result[i][j] = A[i][j] + B[i][j];
            }
        }
        return result;
    }
}

7.6 布尔运算

7.6.1 CSG 布尔运算

构造实体几何(CSG)

通过基本体素的布尔运算构造复杂几何体。

基本运算:

1. 并(Union):A ∪ B
   ┌───┐           ┌───────┐
   │ A │   ∪       │ A ∪ B │
   │   └───┐       │       │
   └───┤ B │   =   └───────┘
       └───┘

2. 交(Intersection):A ∩ B
   ┌───┐           
   │ A │   ∩       ┌─┐
   │   └───┐       │ │  ← A 和 B 的重叠部分
   └───┤ B │   =   └─┘
       └───┘

3. 差(Difference):A - B
   ┌───┐           ┌──┐
   │ A │   -       │  │  ← A 减去与 B 重叠的部分
   │   └───┐       │  │
   └───┤ B │   =   └──┘
       └───┘

7.6.2 网格布尔运算

javascript
/**
 * 网格布尔运算(简化实现)
 * 实际生产中推荐使用专业库如 libigl、CGAL
 */
class MeshBoolean {
    /**
     * 计算两个网格的并集
     */
    static union(meshA, meshB) {
        // 1. 找到所有相交的三角形
        const intersections = this.findIntersections(meshA, meshB);
        
        // 2. 细分相交的三角形
        const subdividedA = this.subdivideAtIntersections(meshA, intersections);
        const subdividedB = this.subdivideAtIntersections(meshB, intersections);
        
        // 3. 分类三角形(在内部/外部)
        const classifiedA = this.classifyTriangles(subdividedA, meshB);
        const classifiedB = this.classifyTriangles(subdividedB, meshA);
        
        // 4. 组合:A 外部 + B 外部
        const result = new TriangleMesh();
        
        for (const tri of classifiedA.outside) {
            result.addTriangleFromMesh(tri);
        }
        for (const tri of classifiedB.outside) {
            result.addTriangleFromMesh(tri);
        }
        
        return result;
    }
    
    /**
     * 计算两个网格的交集
     */
    static intersection(meshA, meshB) {
        // 类似并集,但保留 A 内部 + B 内部
        // ...
    }
    
    /**
     * 计算 A - B(差集)
     */
    static difference(meshA, meshB) {
        // 保留 A 外部 + B 内部(翻转法向)
        // ...
    }
    
    /**
     * 找到两个网格之间的所有三角形相交
     */
    static findIntersections(meshA, meshB) {
        const intersections = [];
        
        // 使用 BVH 加速
        const bvhA = new BVH(meshA);
        const bvhB = new BVH(meshB);
        
        // 找到可能相交的三角形对
        const candidates = bvhA.intersectBVH(bvhB);
        
        // 精确测试每对三角形
        for (const [triA, triB] of candidates) {
            const intersection = this.triangleTriangleIntersection(triA, triB);
            if (intersection) {
                intersections.push(intersection);
            }
        }
        
        return intersections;
    }
    
    /**
     * 三角形与三角形相交测试
     */
    static triangleTriangleIntersection(triA, triB) {
        // Möller 算法或其他方法
        // 返回交线段或 null
        // ...
        return null;
    }
    
    /**
     * 判断点是否在网格内部
     */
    static isPointInsideMesh(point, mesh) {
        // 射线法:从点发射射线,计算与网格的交点数
        // 奇数个交点 → 在内部
        // 偶数个交点 → 在外部
        
        const ray = {
            origin: point,
            direction: { x: 1, y: 0, z: 0 }  // 任意方向
        };
        
        let count = 0;
        for (let i = 0; i < mesh.indices.length; i += 3) {
            const v0 = mesh.vertices[mesh.indices[i]];
            const v1 = mesh.vertices[mesh.indices[i + 1]];
            const v2 = mesh.vertices[mesh.indices[i + 2]];
            
            if (this.rayTriangleIntersection(ray, v0, v1, v2)) {
                count++;
            }
        }
        
        return count % 2 === 1;
    }
}

7.7 本章小结

核心概念

概念说明应用
多边形网格顶点、边、面组成的结构实时渲染
半边结构方便查询邻接关系的数据结构网格编辑
细分曲面递归细分生成光滑曲面动画、电影
网格简化减少面数保持外观LOD
布尔运算网格的交、并、差CAD

数据结构对比

结构存储复杂度邻接查询编辑效率
索引面集O(V + F)O(F)
半边结构O(E)O(1)
邻接列表O(V + E)O(degree)

关键要点

  1. 多边形网格是最常用的几何表示
  2. 半边结构方便邻接查询和网格编辑
  3. Catmull-Clark 是最常用的细分方案
  4. QEM 是高质量网格简化的标准方法
  5. 布尔运算在 CAD 中广泛应用

下一章预告:在第8章中,我们将学习可见性与遮挡算法,包括深度缓冲、背面剔除和遮挡剔除。


文档版本:v1.0
字数统计:约 11,000 字

如有转载或 CV 的请标注本站原文地址