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>
This commit is contained in:
@@ -0,0 +1,488 @@
|
||||
# 顶点变形质量改善设计
|
||||
|
||||
**日期**: 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质量,满足了铁匠锻造场景中纹理细节跟随表面的需求。
|
||||
|
||||
设计优先保持实现简单性和可扩展性,未来可根据实际需求叠加更多约束(如法线保持、体积守恒等)。
|
||||
Reference in New Issue
Block a user