From 5e237b84663aa8d5e31f11f013d77426ccc723c5 Mon Sep 17 00:00:00 2001 From: zhangqing Date: Tue, 20 Jan 2026 19:23:27 +0800 Subject: [PATCH] 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 --- ...01-20-vertex-deformation-quality-design.md | 488 ++++++++++++++++++ 1 file changed, 488 insertions(+) create mode 100644 docs/plans/2026-01-20-vertex-deformation-quality-design.md diff --git a/docs/plans/2026-01-20-vertex-deformation-quality-design.md b/docs/plans/2026-01-20-vertex-deformation-quality-design.md new file mode 100644 index 0000000..68a3636 --- /dev/null +++ b/docs/plans/2026-01-20-vertex-deformation-quality-design.md @@ -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 OriginalEdgeLengths; +``` + +### 内存开销 + +- 假设10万条边 → 100k × 8字节 = **~780KB** +- 在可接受范围内 + +--- + +## 算法实现 + +### 函数1:边长约束(ConstrainEdgeLengths) + +**职责**:遍历所有边,修复超出拉伸/压缩阈值的边 + +```cpp +void ADynamicIronBlock::ConstrainEdgeLengths( + FDynamicMesh3& EditMesh, + TArray& 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& Positions) +{ + TArray SmoothedPositions = Positions; // 复制避免顺序依赖 + + for (FVertexID VertexID : EditMesh.VertexIndicesItr()) + { + // 获取one-ring邻居 + TArray 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 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 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质量,满足了铁匠锻造场景中纹理细节跟随表面的需求。 + +设计优先保持实现简单性和可扩展性,未来可根据实际需求叠加更多约束(如法线保持、体积守恒等)。