【3D-AICG 系列-2】Trellis 2 的O-voxel (上) Shape: Flexible Dual Grid

系列文章目录


文章目录

  • 系列文章目录
  • [Flexible Dual Grid:Mesh ↔ O-Voxel 双向转换详解](#Flexible Dual Grid:Mesh ↔ O-Voxel 双向转换详解)
    • 概述
    • [Part 1: Mesh → O-Voxel(编码)](#Part 1: Mesh → O-Voxel(编码))
      • 整体流程
      • [Step 1: 构建原始网格与对偶网格](#Step 1: 构建原始网格与对偶网格)
      • [Step 2: 收集 Hermite 数据](#Step 2: 收集 Hermite 数据)
      • [Step 3: 最小化 QEF 求解对偶顶点](#Step 3: 最小化 QEF 求解对偶顶点)
    • [Part 2: O-Voxel → Mesh(解码)](#Part 2: O-Voxel → Mesh(解码))
      • 整体流程
      • [Step 1: 找到活跃边和对偶顶点](#Step 1: 找到活跃边和对偶顶点)
      • [Step 2: 连接四边形](#Step 2: 连接四边形)
      • [Step 3: 灵活分割为三角形](#Step 3: 灵活分割为三角形)
    • 完整数据流
    • 核心优势
    • 代码索引

由于 Trellis 2 中的 O-voxel 分为 Shape 和 Material 两个部分,本文先聚焦于 Shape: Flexible Dual Grid 这部分。

Flexible Dual Grid:Mesh ↔ O-Voxel 双向转换详解

基于 TRELLIS 2 源码的算法解析

概述

Flexible Dual Grid 实现了 MeshO-Voxel 之间的高效双向转换:

复制代码
┌────────────┐                              ┌────────────┐
│            │  ═══► 编码 (Encode) ═══►     │             │
│   Mesh     │                              │  O-Voxel   │
│ (三角面片)  │  ◄═══ 解码 (Decode) ◄═══      │ (稀疏体素)  │
└────────────┘                              └────────────┘

Part 1: Mesh → O-Voxel(编码)

整体流程

复制代码
┌─────────────────┐    ┌─────────────────┐    ┌─────────────────┐
│ 1. 构建网格      │     │ 2. 收集数据     │     │ 3. 优化位置     │
│    Construct    │ ─► │    Get Hermite  │ ─► │    Minimize     │
│    Grid         │    │    Data         │    │    QEF          │
└─────────────────┘    └─────────────────┘    └─────────────────┘

Step 1: 构建原始网格与对偶网格

目标:找出所有被 Mesh 表面穿过的体素

算法:扫描线填充(从 X/Y/Z 三个方向)

cpp 复制代码
// flexible_dual_grid.cpp: intersect_qef() 第 96-170 行
auto scan_line_fill = [&](const int ax2) {
    // 对每个三角形,从 ax2 方向扫描
    // 找出所有与网格边相交的体素
    for (int y_idx = start; y_idx < end; ++y_idx) {
        for (int x_idx = line_start; x_idx < line_end; ++x_idx) {
            // 计算交点 z 坐标
            double z = lerp(...);
            // 标记 4 个相邻体素
            for (dx, dy in {0,1} × {0,1}) {
                hash_table[coord] = voxels.size();
                voxels.push_back(coord);
            }
        }
    }
};
scan_line_fill(0);  // 从 X 方向
scan_line_fill(1);  // 从 Y 方向
scan_line_fill(2);  // 从 Z 方向

输出

  • voxels[] - 所有被占据的体素坐标
  • hash_table - 坐标 → 索引的映射

Step 2: 收集 Hermite 数据

Hermite 数据包含:

  1. 边相交标记 intersected[i] - 体素的哪条边与表面相交
  2. 交点均值 means[i] - 所有交点的平均位置
  3. QEF 矩阵 qefs[i] - 用于后续优化的误差函数
cpp 复制代码
// 三种 QEF 来源:

// 1. 边相交 QEF(主要)
intersect_qef(...);   // 第 61-172 行
// Q = plane × planeᵀ,其中 plane = [n, -n·v₀]

// 2. 面 QEF(补充贴合度)
face_qef(...);        // 第 175-288 行
// 对与三角形相交的体素累加 Q

// 3. 边界 QEF(处理开放网格)
boundry_qef(...);     // 第 291-383 行
// 只被 1 个三角形使用的边 = 边界边
// Q = I - d·dᵀ(到直线距离)

边界边检测

cpp 复制代码
// 第 530-537 行
std::map<pair<int,int>, int> edge_count;
for (每个三角形的 3 条边) {
    edge_count[{v0, v1}]++;
}
// edge_count == 1 → 边界边

Step 3: 最小化 QEF 求解对偶顶点

目标:找到体素内最优的对偶顶点位置

数学形式

复制代码
minimize    E(v) = vᵀ Q v
subject to  v ∈ [voxel_min, voxel_max]

求解策略(第 561-765 行):

cpp 复制代码
// 1. 尝试无约束解
Eigen::Vector3f v = A.solve(b);  // A = Q[0:3,0:3], b = -Q[0:3,3]

if (v 在体素内) {
    return v;  // 直接使用
}

// 2. 约束求解(解在体素外时)
float best_error = ∞;

// 2.1 面约束(固定 1 个坐标)→ 6 种情况
for (axis in {x,y,z}, bound in {min,max}) {
    solve_2D_on_face();
}

// 2.2 边约束(固定 2 个坐标)→ 12 种情况  
for (free_axis in {x,y,z}, 4种边界组合) {
    solve_1D_on_edge();
}

// 2.3 角约束(固定 3 个坐标)→ 8 个角点
for (8 corners) {
    evaluate_error();
}

return argmin(best_error);

直观理解

复制代码
无约束解在外部时:
┌─────────┐
│         │      × 无约束解
│         │     ↗
│         │    /
│    •────┼───/── 约束后的解(在面上)
└─────────┘

Part 2: O-Voxel → Mesh(解码)

整体流程

复制代码
┌─────────────────┐    ┌─────────────────┐    ┌─────────────────┐
│ 1. 找活跃边     │    │ 2. 连接四边形   │    │ 3. 分割三角形   │
│    Find Active  │ ─► │    Connect      │ ─► │    Split        │
│    Edges        │    │    Quads        │    │    Triangles    │
└─────────────────┘    └─────────────────┘    └─────────────────┘

Step 1: 找到活跃边和对偶顶点

活跃边 :被表面穿过的体素边,由 intersected[] 标记

cpp 复制代码
// flexible_dual_grid.cpp: face_from_dual_vertices() 第 425-427 行
for (int i = 0; i < dual_vertices.size(); ++i) {
    int3 coord = voxels[i];
    bool3 is_intersected = intersected[i];
    // is_intersected = {x边活跃?, y边活跃?, z边活跃?}
}

图示

复制代码
      z
      │
      │    • 对偶顶点
  ────┼────────
     /│╲
    / │ ╲ ← 活跃边(intersected[2]=true 表示 z 方向边活跃)
   /  │  ╲

Step 2: 连接四边形

原理:每条活跃边连接 4 个相邻体素的对偶顶点,形成一个四边形

cpp 复制代码
// 第 429-456 行
// 查找 6 个可能的邻居
size_t neigh[6] = {
    hash[{x+1, y,   z  }],   // 0: +x
    hash[{x,   y+1, z  }],   // 1: +y
    hash[{x+1, y+1, z  }],   // 2: +x+y
    hash[{x,   y,   z+1}],   // 3: +z
    hash[{x+1, y,   z+1}],   // 4: +x+z
    hash[{x,   y+1, z+1}],   // 5: +y+z
};

// z 轴边活跃 → 连接 xy 平面的 4 个体素
if (intersected[2] && neigh[0,1,2] 都存在) {
    quad = {i, neigh[0], neigh[2], neigh[1]};
}

图示

复制代码
        neigh[1]─────neigh[2]
           │    ╲   ╱    │
           │     ╲ ╱     │
           │      ╳      │  ← 四边形(由 4 个对偶顶点组成)
           │     ╱ ╲     │
           │    ╱   ╲    │
           i ─────────neigh[0]
                 ↑
            活跃的 z 边

Step 3: 灵活分割为三角形

问题:四边形有两种分割方式,哪种更好?

复制代码
方案 A (对角线 AC)        方案 B (对角线 BD)
    A───B                     A───B
    │╲  │                     │  ╱│
    │ ╲ │                     │ ╱ │
    │  ╲│                     │╱  │
    D───C                     D───C

决策标准:选择两个三角形法向量更对齐的分割

cpp 复制代码
// quad_to_2tri() 第 386-415 行
// 计算两种分割的法向量夹角
Eigen::Vector3f n_ABC = (B-A).cross(C-A).normalized();
Eigen::Vector3f n_ACD = (C-A).cross(D-A).normalized();
float angle_AC = acos(n_ABC.dot(n_ACD));

Eigen::Vector3f n_ABD = (B-A).cross(D-A).normalized();
Eigen::Vector3f n_BCD = (C-B).cross(D-B).normalized();
float angle_BD = acos(n_ABD.dot(n_BCD));

// 选择夹角更小的(更平滑)
if (angle_AC <= angle_BD) {
    return {△ABC, △ACD};
} else {
    return {△ABD, △BCD};
}

直观理解

复制代码
好的分割(法向量对齐)      差的分割(法向量不对齐)
      ↗ ↗                        ↗ ↙
     ╱╲                          ╱╲
    ╱  ╲                        ╱  ╲
   ╱────╲                      ╱────╲

→ 表面平滑                    → 表面有折痕

完整数据流

复制代码
          ╔═══════════════════════════════════════════════════════════╗
          ║                    Mesh → O-Voxel                         ║
          ╠═══════════════════════════════════════════════════════════╣
          ║                                                           ║
          ║   vertices ──┬──► intersect_qef() ──► voxels[]           ║
          ║   faces    ──┘          │              hash_table         ║
          ║                         │              means[]            ║
          ║                         │              intersected[]      ║
          ║                         ▼              qefs[]             ║
          ║                    face_qef()                             ║
          ║                         │                                 ║
          ║                         ▼                                 ║
          ║                   boundry_qef()                           ║
          ║                         │                                 ║
          ║                         ▼                                 ║
          ║                   QEF Solve ──────► dual_vertices[]       ║
          ║                                                           ║
          ╠═══════════════════════════════════════════════════════════╣
          ║                                                           ║
          ║   输出 O-Voxel = {voxels, dual_vertices, intersected}    ║
          ║                                                           ║
          ╚═══════════════════════════════════════════════════════════╝

          ╔═══════════════════════════════════════════════════════════╗
          ║                    O-Voxel → Mesh                         ║
          ╠═══════════════════════════════════════════════════════════╣
          ║                                                           ║
          ║   voxels ─────────┐                                       ║
          ║   dual_vertices ──┼──► face_from_dual_vertices()         ║
          ║   intersected ────┘            │                          ║
          ║                                │ (找活跃边,连接 quad)    ║
          ║                                ▼                          ║
          ║                         quad_to_2tri()                    ║
          ║                                │ (智能分割)               ║
          ║                                ▼                          ║
          ║                    输出 Mesh = {vertices, faces}          ║
          ║                                                           ║
          ╚═══════════════════════════════════════════════════════════╝

核心优势

特性 说明
无需 SDF 直接从 Mesh 转换,跳过昂贵的距离场计算
支持开放网格 boundry_qef 专门处理非封闭表面
亚体素精度 对偶顶点可在体素内连续移动
可逆转换 Mesh ↔ O-Voxel 双向无损
智能分割 根据几何特征选择最优三角化

代码索引

功能 函数 位置
扫描线体素化 intersect_qef() 第 61-172 行
面 QEF 累加 face_qef() 第 175-288 行
边界 QEF 累加 boundry_qef() 第 291-383 行
QEF 求解 主函数内 第 561-765 行
四边形分割 quad_to_2tri() 第 386-415 行
连接对偶顶点 face_from_dual_vertices() 第 418-458 行

本文基于 TRELLIS.2/o-voxel/src/convert/flexible_dual_grid.cpp 源码分析

相关推荐
珠海西格电力科技2 小时前
微电网控制策略基础:集中式、分布式与混合式控制逻辑
网络·人工智能·分布式·物联网·智慧城市·能源
Java后端的Ai之路3 小时前
【RAG技术】- RAG系统调优手段之高效召回(通俗易懂附案例)
人工智能·rag·rag系统·召回·rag调优
草莓熊Lotso3 小时前
Linux 基础 IO 初步解析:从 C 库函数到系统调用,理解文件操作本质
linux·运维·服务器·c语言·数据库·c++·人工智能
梵刹古音3 小时前
【C语言】 字符数组相关库函数
c语言·开发语言·算法
Cx330❀3 小时前
从零实现Shell命令行解释器:原理与实战(附源码)
大数据·linux·数据库·人工智能·科技·elasticsearch·搜索引擎
AAD555888998 小时前
数字仪表LCD显示识别与读数:数字0-9、小数点及单位kwh检测识别实战
python
开源技术10 小时前
Python Pillow 优化,打开和保存速度最快提高14倍
开发语言·python·pillow
Niuguangshuo10 小时前
深入解析Stable Diffusion基石——潜在扩散模型(LDMs)
人工智能·计算机视觉·stable diffusion
迈火10 小时前
SD - Latent - Interposer:解锁Stable Diffusion潜在空间的创意工具
人工智能·gpt·计算机视觉·stable diffusion·aigc·语音识别·midjourney