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

13 KiB
Raw Blame History

顶点变形质量改善设计

日期: 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)

// ========== 网格质量改善数据 ==========

// 边长约束参数
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)

职责:遍历所有边,修复超出拉伸/压缩阈值的边

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)

职责:将每个顶点向其邻居的重心移动,改善拓扑流畅性

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行):

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() 中添加原始边长缓存:

// 保存原始顶点位置后,缓存原始边长
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:脏标记系统(未来扩展)

当前:每次锤击处理整个网格 优化:只处理变形区域附近的顶点

TSet<FVertexID> DirtyVertices;  // 受影响的顶点集合

void MarkDirtyVerticesInRadius(FVector ImpactCenter, float Radius)
{
    // 标记半径内的所有顶点为"脏"
}

// 在质量改善阶段只处理脏顶点

预期收益:如果变形区域<30%网格,性能提升2-3倍

实现优先级:低(等性能瓶颈出现再优化)


优化3:并行化(可选)

使用 ParallelFor 并行处理独立的边组。

复杂度:高(需要边着色算法分组) 收益:约1.5-2倍加速 建议:先验证串行版本,性能不足再考虑


性能监控

添加分段计时日志:

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质量,满足了铁匠锻造场景中纹理细节跟随表面的需求。

设计优先保持实现简单性和可扩展性,未来可根据实际需求叠加更多约束(如法线保持、体积守恒等)。