Files
BladeSmith/docs/plans/2026-01-20-vertex-deformation-quality-design.md
T
zhangqing 5e237b8466 Add vertex deformation quality improvement design
Design document for improving mesh topology and UV quality during
plastic deformation. Uses Laplacian smoothing + edge length constraints
to prevent triangle degradation and texture tearing.

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-01-20 19:23:27 +08:00

489 lines
13 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 顶点变形质量改善设计
**日期**: 2026-01-20
**关联计划**: graceful-swimming-rainbow.md(塑性变形体素系统)
**问题**: 三角形顶点不规则位移导致布线与UV质量下降
---
## 问题陈述
在当前的塑性变形系统设计中,顶点直接从体素位移场采样(三线性插值)并应用到网格。这种独立采样会导致:
1. **三角形退化**:顶点各自移动,可能导致三角形翻转、极扁平或自相交
2. **UV拉伸/撕裂**:UV坐标固定但3D位置大幅移动,纹理严重扭曲
3. **布线质量下降**:原本规则的网格流变得混乱
### 项目约束
- **性能预算**:<2ms 额外开销
- **UV要求**:必须保持连续性,纹理细节(划痕、锈迹)需跟随表面变形
- **网格精度**:0.5mm初始精度,高面数模型(~10万顶点)
- **拓扑**:优先保持原始拓扑,可选动态细分
---
## 方案选择
### 备选方案
**方案A:拉普拉斯平滑 + 边长保护** ✅ 已选择
- 性能:~1.6ms
- UV质量:中上
- 实现复杂度:低
**方案B:分区域刚性约束(Rigid Clusters)**
- 性能:~1.5-2ms
- UV质量:高
- 实现复杂度:高
**方案C:简化版 - 仅边长约束**
- 性能:~0.5-0.8ms
- UV质量:中
- 实现复杂度:最低
### 选择理由
方案A在性能、质量和复杂度之间取得最佳平衡:
- 性能可靠(远低于2ms预算)
- UV质量足够(轻微扭曲在铁匠场景中可接受)
- 实现简单,可快速验证
- 易于扩展(未来可叠加其他约束)
---
## 架构设计
### 整体流程
```
现有流程:
体素位移场 → 三线性插值采样 → 直接应用到顶点 → 重算法线
新流程:
体素位移场 → 三线性插值采样 → 【质量改善阶段】 → 应用到顶点 → 重算法线
↓
1. 边长约束检查
2. 拉普拉斯平滑(2-3次迭代)
3. 再次边长约束
```
### 执行顺序理由
**"约束 → 平滑 → 约束"三明治结构:**
1. **第一次约束**:修复体素采样产生的极端拉伸/压缩
2. **平滑**:改善整体拓扑流畅性,但可能引入新的小幅违规
3. **第二次约束**:清理平滑阶段带来的边界效应
---
## 数据结构设计
### 新增成员变量(DynamicIronBlock.h)
```cpp
// ========== 网格质量改善数据 ==========
// 边长约束参数
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Deformation Quality")
float MaxEdgeStretchRatio = 1.35f; // 边最大拉伸比(135%)
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Deformation Quality")
float MaxEdgeCompressRatio = 0.70f; // 边最大压缩比(70%)
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Deformation Quality")
int32 EdgeConstraintIterations = 4; // 边长约束迭代次数
// 拉普拉斯平滑参数
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Deformation Quality")
float LaplacianSmoothingWeight = 0.45f; // 平滑强度(0-1)
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Deformation Quality")
int32 LaplacianIterations = 2; // 平滑迭代次数
// 是否启用质量改善
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Deformation Quality")
bool bEnableDeformationQualityImprovement = true;
// 缓存:原始边长(用于约束检查)
TMap<FEdgeID, float> OriginalEdgeLengths;
```
### 内存开销
- 假设10万条边 → 100k × 8字节 = **~780KB**
- 在可接受范围内
---
## 算法实现
### 函数1:边长约束(ConstrainEdgeLengths)
**职责**:遍历所有边,修复超出拉伸/压缩阈值的边
```cpp
void ADynamicIronBlock::ConstrainEdgeLengths(
FDynamicMesh3& EditMesh,
TArray<FVector3d>& Positions)
{
for (int iter = 0; iter < EdgeConstraintIterations; iter++)
{
int32 ViolationCount = 0;
for (FEdgeID EdgeID : EditMesh.EdgeIndicesItr())
{
FIndex2i EdgeVerts = EditMesh.GetEdgeV(EdgeID);
FVertexID V0 = EdgeVerts.A;
FVertexID V1 = EdgeVerts.B;
FVector3d Pos0 = Positions[V0];
FVector3d Pos1 = Positions[V1];
float CurrentLength = (Pos1 - Pos0).Length();
float OriginalLength = OriginalEdgeLengths[EdgeID];
float Ratio = CurrentLength / OriginalLength;
// 检查是否违反约束
if (Ratio > MaxEdgeStretchRatio || Ratio < MaxEdgeCompressRatio)
{
ViolationCount++;
// 计算目标长度(限制在允许范围内)
float TargetLength = OriginalLength * FMath::Clamp(
Ratio,
MaxEdgeCompressRatio,
MaxEdgeStretchRatio
);
// 调整两端顶点(各承担50%)
FVector3d EdgeDir = (Pos1 - Pos0).GetSafeNormal();
FVector3d EdgeCenter = (Pos0 + Pos1) * 0.5;
Positions[V0] = EdgeCenter - EdgeDir * (TargetLength * 0.5);
Positions[V1] = EdgeCenter + EdgeDir * (TargetLength * 0.5);
}
}
// 如果没有违反约束,提前退出
if (ViolationCount == 0) break;
}
}
```
**关键设计点:**
- 从边的中心向两端调整(避免偏向某一端)
- 迭代式收敛(通常2-3次足够)
- 早停机制(无违规则提前结束)
---
### 函数2:拉普拉斯平滑(LaplacianSmoothDeformation)
**职责**:将每个顶点向其邻居的重心移动,改善拓扑流畅性
```cpp
void ADynamicIronBlock::LaplacianSmoothDeformation(
FDynamicMesh3& EditMesh,
TArray<FVector3d>& Positions)
{
TArray<FVector3d> SmoothedPositions = Positions; // 复制避免顺序依赖
for (FVertexID VertexID : EditMesh.VertexIndicesItr())
{
// 获取one-ring邻居
TArray<FVertexID> Neighbors;
EditMesh.GetVtxNbrVerts(VertexID, Neighbors);
if (Neighbors.Num() == 0) continue;
// 计算邻居重心
FVector3d Centroid = FVector3d::Zero();
for (FVertexID NeighborID : Neighbors)
{
Centroid += Positions[NeighborID];
}
Centroid /= (double)Neighbors.Num();
// 向重心移动(加权混合)
float Weight = LaplacianSmoothingWeight;
// 边界顶点使用更低权重(避免收缩)
if (EditMesh.IsBoundaryVertex(VertexID))
{
Weight *= 0.5f;
}
SmoothedPositions[VertexID] = FMath::Lerp(
Positions[VertexID],
Centroid,
Weight
);
}
Positions = SmoothedPositions;
}
```
**关键设计点:**
- 使用临时数组避免顺序依赖(雅可比迭代)
- 权重0.45是经验值(平衡平滑效果与细节保留)
- 边界顶点降低权重(防止边界收缩)
---
## 集成到现有流程
### 修改 UpdateVertexDeformation()
在 `DynamicIronBlock.cpp` 中修改(原计划第368-400行):
```cpp
void ADynamicIronBlock::UpdateVertexDeformation()
{
UDynamicMeshComponent* DynMeshComp = GetDynamicMeshComponent();
if (!DynMeshComp) return;
UDynamicMesh* DynMesh = DynMeshComp->GetDynamicMesh();
if (!DynMesh) return;
// 修改网格顶点
DynMesh->EditMesh([&](FDynamicMesh3& EditMesh)
{
// === 阶段1:从体素场采样位移 ===
TArray<FVector3d> NewPositions;
NewPositions.SetNum(EditMesh.MaxVertexID());
for (int32 VertexID : EditMesh.VertexIndicesItr())
{
FVector3d OriginalPos = FVector3d(OriginalVertexPositions[VertexID]);
FVector Displacement = SampleDisplacementAtPosition(FVector(OriginalPos));
NewPositions[VertexID] = OriginalPos + FVector3d(Displacement);
}
// === 阶段2:质量改善(可选开关) ===
if (bEnableDeformationQualityImprovement)
{
// 2.1 第一次边长约束
ConstrainEdgeLengths(EditMesh, NewPositions);
// 2.2 拉普拉斯平滑(迭代2-3次)
for (int i = 0; i < LaplacianIterations; i++)
{
LaplacianSmoothDeformation(EditMesh, NewPositions);
}
// 2.3 第二次边长约束(清理平滑引入的新违规)
ConstrainEdgeLengths(EditMesh, NewPositions);
}
// === 阶段3:应用最终位置 ===
for (int32 VertexID : EditMesh.VertexIndicesItr())
{
EditMesh.SetVertex(VertexID, NewPositions[VertexID]);
}
}, EDynamicMeshChangeType::GeneralEdit, EDynamicMeshAttributeChangeFlags::Unknown, false);
// 重新计算法线
FGeometryScriptCalculateNormalsOptions NormalOp;
UGeometryScriptLibrary_MeshNormalsFunctions::RecomputeNormals(DynMesh, NormalOp);
DynMeshComp->NotifyMeshUpdated();
}
```
### 初始化修改
在 `InitializeBoxMesh()` 中添加原始边长缓存:
```cpp
// 保存原始顶点位置后,缓存原始边长
OriginalEdgeLengths.Empty();
DynMesh->EditMesh([&](FDynamicMesh3& EditMesh)
{
for (FEdgeID EdgeID : EditMesh.EdgeIndicesItr())
{
FVector3d V0 = EditMesh.GetVertex(EditMesh.GetEdgeV(EdgeID).A);
FVector3d V1 = EditMesh.GetVertex(EditMesh.GetEdgeV(EdgeID).B);
float Length = (V1 - V0).Length();
OriginalEdgeLengths.Add(EdgeID, Length);
}
});
```
---
## 性能优化策略
### 优化1:边界顶点特殊处理 ✅ 已集成
降低边界顶点的平滑权重,防止边界收缩。
**性能影响**:可忽略
---
### 优化2:脏标记系统(未来扩展)
**当前**:每次锤击处理整个网格
**优化**:只处理变形区域附近的顶点
```cpp
TSet<FVertexID> DirtyVertices; // 受影响的顶点集合
void MarkDirtyVerticesInRadius(FVector ImpactCenter, float Radius)
{
// 标记半径内的所有顶点为"脏"
}
// 在质量改善阶段只处理脏顶点
```
**预期收益**:如果变形区域<30%网格,性能提升2-3倍
**实现优先级**:低(等性能瓶颈出现再优化)
---
### 优化3:并行化(可选)
使用 `ParallelFor` 并行处理独立的边组。
**复杂度**:高(需要边着色算法分组)
**收益**:约1.5-2倍加速
**建议**:先验证串行版本,性能不足再考虑
---
### 性能监控
添加分段计时日志:
```cpp
double T0 = FPlatformTime::Seconds();
// 采样位移
double T1 = FPlatformTime::Seconds();
UE_LOG(LogTemp, Warning, TEXT("Displacement Sampling: %.2f ms"), (T1-T0)*1000);
ConstrainEdgeLengths(...);
double T2 = FPlatformTime::Seconds();
UE_LOG(LogTemp, Warning, TEXT("Edge Constraints: %.2f ms"), (T2-T1)*1000);
// ... 其他阶段
```
---
## 测试验证
### 单元测试
**测试1:边长约束正确性**
- 创建简单网格,人为拉伸某条边到200%
- 验证:拉伸比被限制到135%,迭代次数<5
**测试2:拉普拉斯平滑效果**
- 随机扰动顶点位置(±5cm噪声)
- 验证:扰动幅度降低>60%
---
### 集成测试
**测试3:冷态锤击(塑性=0)**
- 验证:顶点位移<0.1mm,质量改善不引入额外变形
**测试4:热态锤击(塑性=1)**
- 验证:
- 冲击点凹陷深度>2mm
- 边长变化率<35%
- 无三角形翻转
- UV纹理连续
**测试5:连续锤击压力测试**
- 同一位置连续锤击10次
- 验证:每次耗时<7ms,无性能衰减
---
### 视觉验证
**测试6:UV完整性检查**
- 使用棋盘格纹理
- 验证:变形后棋盘格连续(无撕裂/重叠)
**测试7:纹理细节跟随测试**
- 绘制划痕纹理
- 验证:划痕跟随表面变形,不发生偏移
---
### 性能基准测试
**目标配置:**
- 网格:10万顶点,20万三角形
- 体素分辨率:50×50×50
- 锤击影响半径:5cm
**目标指标:**
- 边长约束:<1.5ms
- 拉普拉斯平滑:<1.5ms
- 总耗时:<7ms(满足60fps)
**降级策略:**
1. `LaplacianIterations` 从2降到1
2. `EdgeConstraintIterations` 从4降到3
3. 启用脏标记系统
---
## 实现清单
### 文件修改
**DynamicIronBlock.h**
- 添加成员变量:`OriginalEdgeLengths` 及质量改善参数
- 添加函数声明:`ConstrainEdgeLengths()`, `LaplacianSmoothDeformation()`
**DynamicIronBlock.cpp**
- 实现两个质量改善函数
- 修改 `UpdateVertexDeformation()` 添加质量改善阶段
- 修改 `InitializeBoxMesh()` 缓存原始边长
---
## 风险与缓解
### 风险1:性能超出预算
**缓解**:
- 降低迭代次数
- 实现脏标记系统(局部处理)
- 添加性能监控日志,定位瓶颈
---
### 风险2:平滑过度导致细节丢失
**缓解**:
- 暴露所有参数到Blueprint,实时调整
- 降低 `LaplacianSmoothingWeight`
- 减少 `LaplacianIterations`
---
### 风险3:边界收缩或扭曲
**缓解**:
- 已实现边界顶点特殊处理(降低权重)
- 可选:完全跳过边界顶点的平滑
---
## 总结
本设计通过在体素位移采样后添加三步质量改善流程(约束→平滑→约束),在合理的性能预算(~2ms)内显著改善了顶点变形的拓扑和UV质量,满足了铁匠锻造场景中纹理细节跟随表面的需求。
设计优先保持实现简单性和可扩展性,未来可根据实际需求叠加更多约束(如法线保持、体积守恒等)。