Files
BladeSmith/docs/design/01-刀刃形成-各向异性流动设计.md
T
starmen1265 a9543b5909 docs: 添加几何侧技术设计(刀刃形成/表面磨损/网格质量)
三个对应几何与外观的技术设计。design/01 已包含评估增补(参数语义统一、双区实现回填、流水线协作、形变连续性、碰撞渲染同步、体积守恒策略、性能预算协调),修复了 InverseTransformPosition 法线转换 bug。

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
2026-08-11 08:58:26 +08:00

42 KiB
Raw Blame History

刀刃形成-各向异性流动设计

项目: BladeSmith 文档类型: 技术设计(专题) 版本: 2.1 最后更新: 2026-08-11 状态: 整合自原 2026-01-22-刀刃形成技术细节.md,2026-08-11 增补参数语义/双区实现/流水线/连续性/同步/体积守恒/性能预算(P0+P1)

本文档描述刀刃形成过程中各向异性材料流动、边缘约束与自适应网格细分的技术实现。需求见 requirements/01-锤击形变与刀刃形成需求.md。总体架构见 00-锻造系统总体技术架构.md。

范围声明:

  • 音效(对应 requirements/01 §3.2 听觉反馈)不在本文档范围,由 WeaponComponent 与音频系统处理
  • 斜向锤击(HammerDir 非垂直场景)当前未覆盖,见 §7 已知限制
  • 网格质量优化的算法细节见 design/03,本文档仅定义协作接口

1. 技术背景

锻造刀刃时,材料需优先沿边缘方向流动而非均匀扩散,形成薄片状结构且保持边缘平整锐利。球形凸起机制无法满足方向性需求,需要各向异性材料流动算法。


2. 各向异性材料流动

2.1 方向检测算法

/**
 * 查找最近的自由边界方向
 * 材料会优先朝向自由边界流动(阻力最小)
 */
FVector FindNearestFreeEdgeDirection(
    const FDynamicMesh3& Mesh,
    const FVector& LocalPos,
    const FVector& BoxHalfSize)
{
    // 计算到6个面的距离
    float DistToEdges[6] = {
        BoxHalfSize.X + LocalPos.X,  // -X面
        BoxHalfSize.X - LocalPos.X,  // +X面
        BoxHalfSize.Y + LocalPos.Y,  // -Y面
        BoxHalfSize.Y - LocalPos.Y,  // +Y面
        BoxHalfSize.Z + LocalPos.Z,  // -Z面
        BoxHalfSize.Z - LocalPos.Z   // +Z面
    };

    // 找最近的边缘
    int32 NearestEdge = 0;
    float MinDist = DistToEdges[0];
    for (int32 i = 1; i < 6; ++i)
    {
        if (DistToEdges[i] < MinDist)
        {
            MinDist = DistToEdges[i];
            NearestEdge = i;
        }
    }

    // 返回朝向最近边缘的方向
    static const FVector EdgeDirections[6] = {
        FVector(-1, 0, 0), FVector(1, 0, 0),
        FVector(0, -1, 0), FVector(0, 1, 0),
        FVector(0, 0, -1), FVector(0, 0, 1)
    };

    return EdgeDirections[NearestEdge];
}

/**
 * 计算材料流动的主方向
 * 主方向 = 朝向边缘 + 垂直于锤击方向
 */
FVector3f CalculatePrimaryFlowDirection(
    const FVector& HammerDir,      // 锤击方向(通常是-Z)
    const FVector& ToNearestEdge)  // 朝向最近边缘的方向
{
    // 移除锤击方向的分量(材料不能向锤头方向流动)
    FVector3f PrimaryFlow = FVector3f(ToNearestEdge) -
        FVector3f(HammerDir) * FVector::DotProduct(ToNearestEdge, HammerDir);

    return PrimaryFlow.GetSafeNormal();
}

/**
 * 计算次要流动方向(垂直于主方向和锤击方向)
 */
FVector3f CalculateSecondaryFlowDirection(
    const FVector& HammerDir,
    const FVector& PrimaryFlow)
{
    return FVector3f::CrossProduct(FVector3f(HammerDir), PrimaryFlow).GetSafeNormal();
}

2.2 各向异性凸起计算

/**
 * 应用各向异性材料流动
 * @param LocalHitPos 锤击位置(局部空间)
 * @param ImpactRadius 锤击影响半径
 * @param HammerDir 锤击方向
 * @param AnisotropyRatio 各向异性比(主方向/次方向的流动强度比)
 */
void ApplyAnisotropicDeformation(
    FDynamicMesh3& Mesh,
    const FVector& LocalHitPos,
    float ImpactRadius,
    const FVector& HammerDir,
    float AnisotropyRatio,  // 默认3.0,主方向流动强度是次方向的3倍
    float BulgeStrength)
{
    // 计算流动方向
    FVector ToNearestEdge = FindNearestFreeEdgeDirection(Mesh, LocalHitPos, BoxHalfSize);
    FVector3f PrimaryFlow = CalculatePrimaryFlowDirection(HammerDir, ToNearestEdge);
    FVector3f SecondaryFlow = CalculateSecondaryFlowDirection(HammerDir, PrimaryFlow);

    // 对每个顶点应用形变
    for (int32 VID : Mesh.VertexIndicesItr())
    {
        FVector VertexPos = Mesh.GetVertex(VID);
        float Distance = FVector::Dist(VertexPos, LocalHitPos);

        if (Distance < ImpactRadius)
        {
            // 将位置投影到流动方向
            float DistAlongPrimary = FVector::DotProduct(
                FVector(VertexPos - LocalHitPos), PrimaryFlow);
            float DistAlongSecondary = FVector::DotProduct(
                FVector(VertexPos - LocalHitPos), SecondaryFlow);

            // 各向异性距离计算
            // 主方向的"有效距离"更小(更容易到达)→ 凸起更大
            float EffectiveDistance = FMath::Sqrt(
                FMath::Square(DistAlongPrimary / AnisotropyRatio) +
                FMath::Square(DistAlongSecondary)
            );

            float DistanceRatio = EffectiveDistance / ImpactRadius;

            if (DistanceRatio < 1.0f)
            {
                // 使用FalloffExponent控制边缘过渡
                float FalloffExponent = 2.0f;
                float BulgeFactor = BulgeStrength *
                    FMath::Pow(1.0f - DistanceRatio, FalloffExponent);

                // 凸起方向 = 锤击方向(材料被挤压向外)
                FVector Displacement = HammerDir * (-BulgeFactor);

                // 应用形变
                Mesh.SetVertex(VID, VertexPos + Displacement);
            }
        }
    }
}

2.3 参数说明

参数 默认值 范围 效果
AnisotropyRatio 3.0 1.0-10.0 主方向流动强度比。越大,边缘拉伸越明显
BulgeStrength 0.5 0.1-2.0 凸起高度。需要配合ImpactRadius调整
ImpactRadius 5.0 1.0-20.0 锤击影响半径(cm)
FalloffExponent 2.0 1.0-4.0 边缘过渡平滑度。越大,边界越锐利

技术调参指南:

  • 均匀流动场景: AnisotropyRatio = 1.0
  • 边缘锻造场景: AnisotropyRatio = 3.0-5.0(明显方向性)
  • 精细整形场景: AnisotropyRatio = 8.0-10.0(极端定向)

2.4 参数语义统一

各向异性形变涉及多个"力度/深度/强度"参数,需明确语义与调用契约,避免接口与实现脱节(本节回应 requirements/01 中"凸起高度强度"等多个参数的语义边界)。

2.4.1 参数定义与归属

参数 单位 范围 语义 来源
ImpactForce 无 0.0-1.0 锤击力度归一化值,由 WeaponComponent 基于锤头速度计算 输入侧
ImpactDepth cm >0 100% 力度下的理论下压深度,由锤子配置决定 输入侧
BulgeStrength 无 0.1-2.0 凸起区流动强度系数,相对下压深度的比例 本系统可调
AnisotropyRatio 无 1.0-10.0 主方向流动强度比(各向异性程度) 本系统可调
BulgeRatio 无 0-2.0 凸起区半径相对 ImpactRadius 的扩展倍数(典型 0.8) 本系统可调
FalloffExponent 无 1.0-4.0 衰减曲线幂次,控制边界过渡锐利度 本系统可调

2.4.2 参数关系公式

ActualDepth = ImpactDepth × Clamp(ImpactForce, 0, 1)
BulgeOuterRadius = ImpactRadius × (1 + BulgeRatio)

压缩区位移(顶点在 ImpactRadius 内):

VertexDeformation = ActualDepth × (1 - (Distance/ImpactRadius)^FalloffExponent)

凸起区位移高度(顶点在 ImpactRadius ~ BulgeOuterRadius 之间):

BulgeHeight = ActualDepth × BulgeStrength × BulgeFalloff
其中 BulgeFalloff = 1 - (EffectiveDistance/(BulgeOuterRadius-ImpactRadius))^FalloffExponent

2.4.3 调用契约

OnHammerImpact(HitLocation, HitNormal, ImpactRadius, ImpactDepth, ImpactForce):

  • 调用方(WeaponComponent)负责传入 ImpactForce(物理引擎计算)与 ImpactDepth(锤子配置)
  • 本函数内部必须使用 ActualDepth = ImpactDepth × ImpactForce,不可单独使用任一参数
  • ImpactForce 必须在 [0,1] 区间,函数内 FMath::Clamp 兜底
  • HitNormal 是世界空间的方向向量(非位置),必须用 InverseTransformVector 转入局部空间

2.5 体积守恒策略

对应 requirements/01 §1.2 锻造过程可视化:材料从一处被挤压,应当在另一处凸起,体积不应凭空产生或消失。这是真实锻造的核心物理直觉。

2.5.1 当前实现的体积误差来源

§3.4 的双区算法基于"Falloff 函数 + 经验系数",并非严格的体积守恒:

来源 影响
压缩区下压体积 ∝ ActualDepth × Falloff × Area 实际"挤压"的材料量
凸起区隆起体积 ∝ ActualDepth × BulgeStrength × BulgeFalloff × Area 实际"凸起"的材料量
70/30 分量分配 凸起区 70% 向上 + 30% 向外,体积计算需考虑径向流失
BulgeStrength 默认 0.5 意味着凸起体积约为下压体积的 50%,剩余 50% 凭空消失

结论: 当前实现是"近似体积守恒",实际体积误差可达 30-50%。在玩家可感知层面通常没问题,但对真实感敏感的玩家可能注意到"材料越打越少"。

2.5.2 守恒策略选项

策略 描述 优点 缺点
A. 不守恒(当前默认) BulgeStrength 是独立可调参数 调参直观,性能无开销 体积误差大,违反直觉
B. 软守恒(推荐) 计算下压体积,反解 BulgeStrength 使凸起体积 ≈ 下压体积 × 系数 物理直觉正确,BulgeStrength 仍有调参空间 每次锤击多一次积分计算
C. 严格守恒 用拉格朗日乘子法约束 完全守恒 计算复杂,性能开销大

2.5.3 推荐:策略 B 软守恒

算法骨架(伪代码):

// 步骤 1: 计算压缩区下压体积(数值积分)
float CompressedVolume = 0.0f;
for (int32 VertexID : CompressedVertices)
{
    float Falloff = 1.0f - FMath::Pow(Distance/ImpactRadius, FalloffExponent);
    CompressedVolume += ActualDepth * Falloff * GetVertexArea(VertexID);
}

// 步骤 2: 反解 BulgeStrength,使凸起体积 ≈ CompressedVolume × ConservationFactor
// 凸起体积 ≈ BulgeStrength × Σ(BulgeFalloff_i × VertexArea_i) × 0.7(向上分量)
// ⇒ BulgeStrength = CompressedVolume × ConservationFactor / (Σ × 0.7)
float BulgeStrengthCorrected = CompressedVolume * ConservationFactor
                              / (SumBulgeFalloffArea * 0.7f);

// 步骤 3: 用校正后的 BulgeStrength 执行形变(同 §3.4)

ConservationFactor 含义:

  • 1.0 = 完全守恒(压缩体积 = 凸起体积)
  • 0.9 = 10% 体积损耗(模拟材料致密化/加工硬化导致的体积微小减少)
  • 0.5 ≈ 当前默认 BulgeStrength 行为(近似)

GetVertexArea(VertexID) 实现: 取顶点邻接三角形面积的 1/3 之和,可在网格拓扑变化后增量更新,无需每次重算。

2.5.4 性能影响

  • 计算下压体积: O(N)(遍历压缩区顶点)
  • 反解 BulgeStrength: O(1)
  • 总额外开销: < 0.5ms(20 万顶点,仅压缩区遍历),可接受

2.5.5 调参建议

场景 ConservationFactor 效果
真实模式 0.9 接近真实物理,体积基本守恒
娱乐模式 0.5-0.7 凸起比真实少,玩家更容易控制形状
夸张模式 1.1+ 凸起比下压多,适合视觉冲击(非物理)

2.5.6 与策略 A 的兼容

策略 B 不破坏现有调参:

  • 当 bEnableVolumeConservation = false(默认): 走策略 A,直接使用配置的 BulgeStrength
  • 当 bEnableVolumeConservation = true: 走策略 B,BulgeStrength 配置值被忽略(或作为初始猜测)

3. 边缘平整约束

3.1 边缘顶点标记

/**
 * 检测并标记边缘顶点
 * 距离任何边界< EdgeThreshold 的顶点
 */
void MarkEdgeVertices(
    FDynamicMesh3& Mesh,
    TSet<int32>& OutEdgeVertices,
    float EdgeThreshold = 0.5f)  // 5mm
{
    FVector BoxHalfSize = GetBoxHalfSize();

    for (int32 VID : Mesh.VertexIndicesItr())
    {
        FVector Pos = Mesh.GetVertex(VID);

        // 检查是否接近任何边界
        bool bNearEdge = false;
        bNearEdge |= FMath::Abs(Pos.X - BoxHalfSize.X) < EdgeThreshold;
        bNearEdge |= FMath::Abs(Pos.X + BoxHalfSize.X) < EdgeThreshold;
        bNearEdge |= FMath::Abs(Pos.Y - BoxHalfSize.Y) < EdgeThreshold;
        bNearEdge |= FMath::Abs(Pos.Y + BoxHalfSize.Y) < EdgeThreshold;
        bNearEdge |= FMath::Abs(Pos.Z - BoxHalfSize.Z) < EdgeThreshold;
        bNearEdge |= FMath::Abs(Pos.Z + BoxHalfSize.Z) < EdgeThreshold;

        if (bNearEdge)
        {
            OutEdgeVertices.Add(VID);
        }
    }
}

3.2 线性约束应用

/**
 * 对边缘顶点应用线性约束
 * 形变后将边缘顶点投影到拟合直线上,保持平整
 */
void ApplyEdgeLinearConstraint(
    FDynamicMesh3& Mesh,
    const TSet<int32>& EdgeVertices)
{
    for (int32 VID : EdgeVertices)
    {
        if (!Mesh.IsVertex(VID))
            continue;

        // 找到此顶点的边缘邻居
        TArray<int32> EdgeNeighbors;
        for (int32 NbrVID : Mesh.VtxVerticesItr(VID))
        {
            if (EdgeVertices.Contains(NbrVID))
            {
                EdgeNeighbors.Add(NbrVID);
            }
        }

        if (EdgeNeighbors.Num() >= 2)
        {
            // 使用主成分分析(PCA)拟合直线
            FVector CurrentPos = Mesh.GetVertex(VID);
            FVector LineOrigin, LineDirection;

            FitLineToPoints(Mesh, EdgeNeighbors, LineOrigin, LineDirection);

            // 投影到拟合线上
            FVector ProjectedPos = ProjectPointToLine(CurrentPos, LineOrigin, LineDirection);

            // 稍微平滑更新(避免突变)
            FVector SmoothPos = FMath::Lerp(CurrentPos, ProjectedPos, 0.5f);

            Mesh.SetVertex(VID, SmoothPos);
        }
    }
}

3.3 PCA直线拟合

/**
 * 使用PCA拟合直线到点集
 */
void FitLineToPoints(
    const FDynamicMesh3& Mesh,
    const TArray<int32>& VertexIDs,
    FVector& OutOrigin,
    FVector& OutDirection)
{
    // 计算质心
    FVector Centroid = FVector::ZeroVector;
    for (int32 VID : VertexIDs)
    {
        Centroid += Mesh.GetVertex(VID);
    }
    Centroid /= VertexIDs.Num();

    // 计算协方差矩阵
    float CovXX = 0, CovYY = 0, CovZZ = 0;
    float CovXY = 0, CovXZ = 0, CovYZ = 0;

    for (int32 VID : VertexIDs)
    {
        FVector P = Mesh.GetVertex(VID) - Centroid;
        CovXX += P.X * P.X;
        CovYY += P.Y * P.Y;
        CovZZ += P.Z * P.Z;
        CovXY += P.X * P.Y;
        CovXZ += P.X * P.Z;
        CovYZ += P.Y * P.Z;
    }

    // 构建协方差矩阵
    FMatrix33 CovMatrix(
        FVector3f(CovXX, CovXY, CovXZ),
        FVector3f(CovXY, CovYY, CovYZ),
        FVector3f(CovXZ, CovYZ, CovZZ)
    );

    // 计算特征向量(主方向 = 最大特征值对应的向量)
    FVector3f EigenValues;
    FMatrix33 EigenVectors;
    FMatrix33::EigenDecomposition(CovMatrix, EigenValues, EigenVectors);

    // 最大特征值对应的列向量是主方向
    OutOrigin = Centroid;
    OutDirection = FVector(EigenVectors.GetColumn(0));  // 第一主成分
}

/**
 * 将点投影到直线上
 */
FVector ProjectPointToLine(
    const FVector& Point,
    const FVector& LineOrigin,
    const FVector& LineDirection)
{
    FVector ToPoint = Point - LineOrigin;
    float ProjectionLength = FVector::DotProduct(ToPoint, LineDirection);
    return LineOrigin + LineDirection * ProjectionLength;
}

3.4 形变核心实现

本系统的形变核心函数。实现要点(基于 impl 实际代码回填):

  • 双区算法: 压缩区(ImpactRadius 内)沿锤击法线下压;凸起区(ImpactRadius~BulgeOuterRadius)材料 70% 向上 + 30% 向外流动
  • 各向异性凸起: 凸起区使用 §2.2 的 EffectiveDistance 公式,主方向凸起更大
  • 累积应变: 每顶点位移量累加到 AccumulatedStrain,供 design/04 硬化模型消费
  • (可选)边缘约束 + 自适应细分 在同一 EditMesh 内执行,避免多次 lock

本函数仅负责"形变核心"。完整的锤击事件流水线(含 design/03 网格优化、design/04-07 物理更新)见 §3.5。

void ADynamicIronBlock::OnHammerImpact(
    FVector HitLocation,
    FVector HitNormal,
    float ImpactRadius,
    float ImpactDepth,
    float ImpactForce)
{
    // 获取组件与网格
    UDynamicMeshComponent* DynMeshComp = GetDynamicMeshComponent();
    if (!DynMeshComp)
    {
        UE_LOG(LogTemp, Error, TEXT("DynamicIronBlock: DynamicMeshComponent is null"));
        return;
    }
    UDynamicMesh* DynMesh = DynMeshComp->GetDynamicMesh();
    if (!DynMesh)
    {
        UE_LOG(LogTemp, Error, TEXT("DynamicIronBlock: DynamicMesh is null"));
        return;
    }

    // 力度归一化(参考 §2.4.3 调用契约)
    ImpactForce = FMath::Clamp(ImpactForce, 0.0f, 1.0f);

    // 世界→局部(注意:HitNormal 是方向向量,必须用 InverseTransformVector,不能用 InverseTransformPosition)
    FTransform ActorTransform = GetActorTransform();
    FVector LocalHitLocation = ActorTransform.InverseTransformPosition(HitLocation);
    FVector LocalHitNormal = ActorTransform.InverseTransformVector(HitNormal).GetSafeNormal();

    // 实际下压深度(参考 §2.4.2)
    float ActualDepth = ImpactDepth * ImpactForce;

    // 计算材料流动方向
    FVector BoxHalfSize = BoxSize * 0.5f;
    FVector ToNearestEdge = FindNearestFreeEdgeDirection(LocalHitLocation, BoxHalfSize);
    FVector PrimaryFlow = CalculatePrimaryFlowDirection(LocalHitNormal, ToNearestEdge);
    FVector SecondaryFlow = CalculateSecondaryFlowDirection(LocalHitNormal, PrimaryFlow);

    float BulgeOuterRadius = ImpactRadius * (1.0f + BulgeRatio);

    DynMesh->EditMesh([&](FDynamicMesh3& EditMesh)
    {
        // ===== 1. 双区形变 =====
        for (int32 VertexID : EditMesh.VertexIndicesItr())
        {
            FVector3d VertexPos = EditMesh.GetVertex(VertexID);
            FVector VertexPosFloat(VertexPos.X, VertexPos.Y, VertexPos.Z);
            float Distance = FVector::Dist(VertexPosFloat, LocalHitLocation);

            if (Distance >= BulgeOuterRadius)
                continue;

            FVector Displacement = FVector::ZeroVector;

            // 1a. 压缩区:ImpactRadius 内,沿锤击法线下压
            if (Distance < ImpactRadius)
            {
                float NormalizedDist = Distance / ImpactRadius;
                float Falloff = 1.0f - FMath::Pow(NormalizedDist, FalloffExponent);
                float VertexDeformation = ActualDepth * Falloff;
                Displacement = -LocalHitNormal * VertexDeformation;
            }
            // 1b. 凸起区:ImpactRadius ~ BulgeOuterRadius,材料向上+向外流动
            else
            {
                float DistAlongPrimary = FVector::DotProduct(
                    FVector(VertexPosFloat - LocalHitLocation), PrimaryFlow);
                float DistAlongSecondary = FVector::DotProduct(
                    FVector(VertexPosFloat - LocalHitLocation), SecondaryFlow);

                // 各向异性距离(主方向"更近" → 凸起更大)
                float EffectiveDistance = FMath::Sqrt(
                    FMath::Square(DistAlongPrimary / AnisotropyRatio) +
                    FMath::Square(DistAlongSecondary)
                );
                float DistanceInBulgeZone = EffectiveDistance / (BulgeOuterRadius - ImpactRadius);

                if (DistanceInBulgeZone < 1.0f)
                {
                    float BulgeFalloff = 1.0f - FMath::Pow(DistanceInBulgeZone, FalloffExponent);
                    float BulgeHeight = ActualDepth * BulgeStrength * BulgeFalloff;

                    // 70% 向上(反法线方向)+ 30% 向外(径向,各向异性)
                    FVector UpwardDisplacement = -LocalHitNormal * BulgeHeight * 0.7f;

                    FVector RadialDir = PrimaryFlow * DistAlongPrimary + SecondaryFlow * DistAlongSecondary;
                    RadialDir.Normalize();
                    FVector OutwardDisplacement = RadialDir * BulgeHeight * 0.3f;

                    Displacement = UpwardDisplacement + OutwardDisplacement;
                }
            }

            // 应用形变 + 累积应变(供 design/04 硬化模型消费)
            if (!Displacement.IsNearlyZero())
            {
                EditMesh.SetVertex(VertexID, VertexPos + FVector3d(Displacement.X, Displacement.Y, Displacement.Z));

                if (AccumulatedStrain.IsValid())
                {
                    float CurrentStrain = AccumulatedStrain->GetValue(VertexID);
                    AccumulatedStrain->SetValue(VertexID, CurrentStrain + Displacement.Size());
                }
            }
        }

        // ===== 2. (可选)边缘平整约束 =====
        // 仅处理受影响区域的边缘顶点,避免全网格遍历
        if (bEnableEdgeConstraint)
        {
            TSet<int32> EdgeVertices;
            MarkEdgeVertices(EditMesh, EdgeVertices, EdgeThreshold);

            TSet<int32> AffectedEdges;
            for (int32 VID : EdgeVertices)
            {
                if (!EditMesh.IsVertex(VID)) continue;
                FVector3d Pos = EditMesh.GetVertex(VID);
                FVector PosFloat(Pos.X, Pos.Y, Pos.Z);
                if (FVector::Dist(PosFloat, LocalHitLocation) < ImpactRadius * 1.5f)
                {
                    AffectedEdges.Add(VID);
                }
            }

            if (AffectedEdges.Num() > 0)
            {
                ApplyEdgeLinearConstraint(EditMesh, AffectedEdges);
            }
        }

        // ===== 3. (可选,降频)自适应细分 =====
        // 详见 §4。每 5 次锤击执行一次,避免过度细分
        if (bEnableAdaptiveRefinement && (HammerHitCount % 5 == 0))
        {
            SubdivideHighCurvatureEdges(EditMesh, 0.5f, 2.0f, 100);
        }
    }, EDynamicMeshChangeType::GeneralEdit, EDynamicMeshAttributeChangeFlags::Unknown, false);

    // ===== 4-6. 法线重算 / 渲染同步 / BVH 重建(详见 §3.7)=====
    FGeometryScriptCalculateNormalsOptions NormalOp;
    UGeometryScriptLibrary_MeshNormalsFunctions::RecomputeNormals(DynMesh, NormalOp);
    DynMeshComp->NotifyMeshUpdated();
    GenerateBVH();

    HammerHitCount++;
}

3.5 锤击事件完整流水线

§3.4 描述了"形变核心"。在实际锤击事件中,形变核心只是完整流水线的第一步。本节定义跨子系统的协作顺序、数据流与调度职责,作为实现的权威参考。

替代关系: 本节细化了 design/00 §3 的 8 步物理流水线在几何处理层面的细节,也补充了 design/03 §8.1 的简化集成示例。当本节与 design/03 §8.1 冲突时,以本节为准。

3.5.1 完整流水线

一次完整的 OnHammerImpact 调用,内部按以下顺序执行(标 ★ 的步骤是 design/01 的职责):

[输入] HitLocation, HitNormal, ImpactRadius, ImpactDepth, ImpactForce
  │
  ▼
0. 输入预处理
   - ImpactForce Clamp 到 [0,1]
   - 世界→局部坐标转换(参考 §3.4)
   - ActualDepth = ImpactDepth × ImpactForce
                 │
                 ▼
1. ★ EditMesh 开始(单次 lock)
   1a. ★ 压缩区 + 凸起区双区形变(§3.4)
   1b. ★ AccumulatedStrain 累积(供 design/04 消费)
   1c. ★ PolishAmount 更新(供 design/02 消费)
   1d. ★ (可选)边缘平整约束(§3)
   1e. ★ (可选,降频)自适应细分(§4)
                 │
                 ▼
2. 网格质量优化(design/03)
   2a. FindAffectedTriangles
   2b. 边翻转(FlipEdgesToImproveQuality)
   2c. Laplacian 平滑(SmoothVertices)
   2d. (降频)FRemesher 重网格(RemeshRegion)
   ─ 可在 EditMesh 内执行,也可独立 lock
                 │
                 ▼
3. EditMesh 提交
                 │
                 ▼
4. 材料状态更新(对应 design/00 §3 物理流水线)
   4a. 硬化计算(design/04,基于 AccumulatedStrain)
   4b. 应力累积(design/06)
   4c. 温度采样→塑性系数(架构层)
   4d. 疲劳累积(design/07)
   4e. 裂纹判定与扩展(design/06/07)
                 │
                 ▼
5. 视觉与材质更新
   5a. 法线重算(RecomputeNormals)
   5b. 顶点颜色更新(温度/打磨度/相态,design/02/05)
   5c. 动态材质参数更新(回火色/氧化层,design/05)
                 │
                 ▼
6. 碰撞与渲染同步(§3.7)
   6a. BVH 重建(GenerateBVH)
   6b. NotifyMeshUpdated / MarkRenderStateDirty
                 │
                 ▼
7. ★ HammerHitCount++(供下次锤击的降频判断)

3.5.2 跨子系统数据契约

design/01 产生以下数据,被其他子系统消费:

产出 类型 消费者 用途
AccumulatedStrain 累积 顶点属性增量 design/04 硬化、design/06 应力、design/07 疲劳 输入
PolishAmount 更新 顶点属性增量 design/02 表面磨损 输入(打磨度增加)
受影响顶点集合 TSet<int32> design/03 网格优化 局部化处理范围
形变后的网格拓扑 FDynamicMesh3 design/03、design/06 裂纹检测 几何基础

3.5.3 与 design/03 网格优化的协作

职责划分:

  • design/01 负责"产生形变"——决定网格顶点位置如何变化
  • design/03 负责"修复退化"——决定网格拓扑如何调整以维持质量

调用方式(两种方案,项目集成时确定):

方案 描述 优点 缺点 适用
A. 同 EditMesh 内串行 design/01 在 EditMesh 内直接调用 design/03 的函数 单次 lock,开销最小 子系统耦合 简化集成
B. 上层调度器解耦(推荐) design/01 完成形变后,由上层调度器调用 design/03 子系统解耦,职责清晰 两次 EditMesh lock,轻微开销 长期可维护

关键要求(无论哪种方案):design/01 必须输出"受影响三角形集合"供 design/03 局部化处理。当前 §3.4 实现里只跟踪了"边缘顶点",需扩展为完整的"受影响三角形"集合(参考 design/03 §8.2 FindAffectedTriangles)。

3.5.4 与 design/00 物理流水线的对应

design/00 §3 定义了 8 步物理流水线。本节是其几何层面的扩展:

design/00 §3 步骤 本节对应步骤 备注
1. 形变计算 §3.5.1 步骤 1 design/01 核心
2. 更新顶点应变累积 §3.5.1 步骤 1b 在 EditMesh 内
3. 从温度体素采样 §3.5.1 步骤 4c 架构层
4. 硬化计算 §3.5.1 步骤 4a design/04
5. 应力累积 §3.5.1 步骤 4b design/06
6. 体素应变率/淬火 §3.5.1 步骤 4(异步 GPU) design/05
7. 疲劳与裂纹 §3.5.1 步骤 4d/4e design/07
8. 材质可视化 §3.5.1 步骤 5 design/02/05

design/00 §3 未覆盖的两个关键步骤(由本节补充):

  • 步骤 2: 网格质量优化(几何层面)
  • 步骤 6: 碰撞与渲染同步(基础设施层面)

3.6 形变连续性保证

对应 requirements/01 §1.4"形变连续性":连续锻打能逐步把铁块变形成目标形状,每次锤击的形变应当是连续的、可预测的,不会出现突变或奇怪形状。

3.6.1 单次锤击的平滑过渡

形变函数通过以下机制保证单次锤击内部的平滑性:

  • FalloffExponent 衰减: 压缩区与凸起区都使用 (1 - ratio)^FalloffExponent 衰减,边界处位移趋近 0,中心最大,确保空间连续
  • 双区衔接: 压缩区(ImpactRadius 内)与凸起区(ImpactRadius~BulgeOuterRadius)在 ImpactRadius 边界处位移都趋近于 0,避免突变
  • 径向归一化: 凸起区 RadialDir.Normalize() 确保方向连续

禁止做法: 在 EditMesh 内对顶点位置做硬切换(如 if (cond) SetVertex(A) else SetVertex(B)),会破坏空间连续性。

3.6.2 多次锤击的累积

  • AccumulatedStrain 单调累加: 每次锤击的位移量累加,不会归零,玩家能感知"金属越打越变形"
  • HardnessCoefficient 反馈(由 design/04 实现): 累积应变越高,下次形变阻力越大(参考 design/04 加工硬化)
  • PolishAmount 单调上升: 打磨度只增不减,表面从生锈到光亮的过渡是单调的

3.6.3 时间稳定性(避免震荡)

  • 无状态依赖: 形变函数不依赖前一帧状态,只依赖当前网格与锤击参数。同一位置多次锤击,效果是单调累积而非震荡
  • 边缘约束的阻尼: §3.2 ApplyEdgeLinearConstraint 中 FMath::Lerp(CurrentPos, ProjectedPos, 0.5f) 用 50% 插值避免突变。若用 100% 投影会导致边缘顶点在多次锤击后跳变

3.6.4 累积退化交接给 design/03

多次锤击后网格质量会下降(细长三角形、面积过小等)。design/01 不直接处理退化,而是通过两个机制交接:

退化类型 检测/触发 处理者
大三角形 / 高曲率边 §4 SubdivideHighCurvatureEdges(曲率 > 30° / 边长 > 2cm) design/01 §4 自适应细分
细长三角形(长宽比 > 10:1) design/03 IsDegenerateTriangle design/03 边翻转 + 重网格
顶点分布不均 design/03 CalculateVertexCurvature design/03 Laplacian 平滑

具体阈值与算法见 design/03。


3.7 碰撞与渲染同步

对应 requirements/03 §5"碰撞检测稳定,不会出现打空或检测错误"与"网格优化过程透明,不影响形变视觉效果"。

3.7.1 BVH 重建

锤击形变改变了网格顶点位置,碰撞检测加速结构必须同步更新,否则下一次锤击会"打空"或检测到错误位置。

当前实现(§3.4 末尾):

GenerateBVH();  // 全量重建

性能特征:

  • 全量重建: 5-10ms(20 万顶点),已在 §5 性能预算内
  • 增量重建(未来改进): 仅更新受影响区域的 BVH 节点,可降至 <2ms

禁止: 在 EditMesh 内调用 GenerateBVH(此时网格还未提交)。必须在 EditMesh 退出后调用。

3.7.2 法线重算

形变改变了网格几何,法线必须重算,否则:

  • 着色错误(表面出现条纹/暗斑)
  • design/02 表面磨损的 WorldNormal 输入错误
FGeometryScriptCalculateNormalsOptions NormalOp;
UGeometryScriptLibrary_MeshNormalsFunctions::RecomputeNormals(DynMesh, NormalOp);

频率: 每次锤击后必须执行。无法降频——法线错误肉眼立即可见。

3.7.3 渲染状态通知

DynamicMesh 的渲染数据有内部缓存,形变后必须显式失效:

DynMeshComp->NotifyMeshUpdated();              // 通知组件网格已更新
DynMeshComp->MarkRenderStateDirty();           // 标记渲染状态脏(强制重建渲染数据)
DynMeshComp->MarkRenderDynamicDataDirty();     // 标记动态数据脏(顶点颜色等)

调用时机: EditMesh 退出之后,法线重算之后,BVH 重建之后。

3.7.4 异步可能性

某些步骤可以延后到帧尾执行,不阻塞锤击响应:

步骤 可否异步 说明
法线重算 ❌ 否 视觉延迟肉眼可见
渲染状态通知 ❌ 否 必须当帧完成
BVH 重建 ✅ 是 下一次锤击前完成即可

BVH 异步期间的冲突处理: 若玩家在 BVH 重建期间再次锤击,需要等待 BVH 完成或使用旧 BVH(可能"打空")。这是性能与响应性的权衡,默认选择"等待"(保证碰撞正确)。


4. 自适应网格细分

4.1 高曲率边缘细分

/**
 * 检测高曲率边缘并细分
 */
void SubdivideHighCurvatureEdges(
    FDynamicMesh3& Mesh,
    float CurvatureThreshold,    // 曲率阈值
    float MaxEdgeLength,         // 最大边长
    int32 MaxSubdivisions)       // 最大细分次数
{
    int32 SubdivisionCount = 0;

    for (int32 EID : Mesh.EdgeIndicesItr())
    {
        if (SubdivisionCount >= MaxSubdivisions)
            break;

        // 跳过边界边
        if (Mesh.IsBoundaryEdge(EID))
            continue;

        FIndex2i EdgeVerts = Mesh.GetEdgeV(EID);
        FVector V0 = Mesh.GetVertex(EdgeVerts.A);
        FVector V1 = Mesh.GetVertex(EdgeVerts.B);
        float EdgeLength = FVector::Dist(V0, V1);

        // 边太长或曲率过高 → 细分
        float EdgeCurvature = CalculateEdgeCurvature(Mesh, EID);

        if (EdgeLength > MaxEdgeLength && EdgeCurvature > CurvatureThreshold)
        {
            // 细分此边
            FDynamicMesh3::FEdgeSplitInfo SplitInfo;
            EMeshResult Result = Mesh.SplitEdge(EID, SplitInfo);

            if (Result == EMeshResult::Ok)
            {
                // 新顶点位置 = 中点
                FVector MidPoint = (V0 + V1) * 0.5f;
                Mesh.SetVertex(SplitInfo.NewVertex, MidPoint);

                // 插值顶点属性
                InterpolateVertexAttributes(Mesh, EdgeVerts.A, EdgeVerts.B, SplitInfo.NewVertex);

                SubdivisionCount++;
            }
        }
    }
}

4.2 曲率计算

/**
 * 计算边的曲率(基于邻面法线夹角)
 */
float CalculateEdgeCurvature(const FDynamicMesh3& Mesh, int32 EdgeID)
{
    FIndex2i EdgeTris = Mesh.GetEdgeT(EdgeID);

    // 边界边曲率为0
    if (EdgeTris.B == IndexConstants::InvalidID)
        return 0.0f;

    // 获取两个三角形的法线
    FVector Normal0 = Mesh.GetTriNormal(EdgeTris.A);
    FVector Normal1 = Mesh.GetTriNormal(EdgeTris.B);

    // 法线夹角 = 曲率度量
    float Dot = FVector::DotProduct(Normal0, Normal1);
    return FMath::Acos(Dot);  // 返回弧度
}

4.3 属性插值

/**
 * 插值顶点属性
 * 确保新顶点继承正确的材料状态
 */
void InterpolateVertexAttributes(
    FDynamicMesh3& Mesh,
    int32 VID0,
    int32 VID1,
    int32 NewVID)
{
    // 位置(已由SplitEdge处理)

    // 初始位置
    FVector InitPos0 = InitialPositions->GetValue(VID0);
    FVector InitPos1 = InitialPositions->GetValue(VID1);
    InitialPositions->SetValue(NewVID, (InitPos0 + InitPos1) * 0.5f);

    // 打磨度
    float Polish0 = PolishAmount->GetValue(VID0);
    float Polish1 = PolishAmount->GetValue(VID1);
    PolishAmount->SetValue(NewVID, (Polish0 + Polish1) * 0.5f);

    // 累积应变
    float Strain0 = AccumulatedStrain->GetValue(VID0);
    float Strain1 = AccumulatedStrain->GetValue(VID1);
    AccumulatedStrain->SetValue(NewVID, (Strain0 + Strain1) * 0.5f);

    // 硬度
    float Hardness0 = HardnessCoefficient->GetValue(VID0);
    float Hardness1 = HardnessCoefficient->GetValue(VID1);
    HardnessCoefficient->SetValue(NewVID, (Hardness0 + Hardness1) * 0.5f);

    // 晶粒尺寸
    float Grain0 = GrainSize->GetValue(VID0);
    float Grain1 = GrainSize->GetValue(VID1);
    GrainSize->SetValue(NewVID, (Grain0 + Grain1) * 0.5f);

    // 顶点颜色
    FColor Color0 = Mesh.GetVertexColor(VID0);
    FColor Color1 = Mesh.GetVertexColor(VID1);
    FLinearColor LinearColor0(Color0);
    FLinearColor LinearColor1(Color1);
    FLinearColor AvgColor = (LinearColor0 + LinearColor1) * 0.5f;
    Mesh.SetVertexColor(NewVID, AvgColor.ToFColor());
}

4.4 集成到锤击流程

void ADynamicIronBlock::OnHammerImpact(...)
{
    // ... 形变计算 ...

    // 自适应细分(每隔几次锤击执行,避免过度细分)
    if (bEnableAdaptiveRefinement && HammerHitCount % 5 == 0)
    {
        SubdivideHighCurvatureEdges(
            *DynamicMesh,
            CurvatureThreshold = 0.5f,  // ~30度角
            MaxEdgeLength = 2.0f,        // 2cm
            MaxSubdivisions = 100
        );
    }
}

5. 性能考虑

5.1 计算复杂度

操作 复杂度 实际耗时(20万顶点)
各向异性凸起 O(N) 2-3ms
边缘约束(PCA) O(N×K) +3-5ms
自适应细分 O(E) +2-4ms
属性插值 O(1) per顶点 可忽略

5.2 优化建议

  1. 限制约束区域: 只对受影响的边缘顶点应用约束
  2. 降低细分频率: 每5-10次锤击才细分一次
  3. LOD策略: 远距离铁块使用简化约束
  4. 异步计算: 细分可以延后到帧尾执行

5.3 性能预算协调策略

对应 requirements/03 §5 验收点"单次锤击总耗时(形变+优化)≤ 7ms"。

§5.1 复杂度表的最坏情况合计 7-12ms,需通过协调策略确保实测落在 7ms 内。

5.3.1 实测耗时 vs 最坏情况

§5.1 的复杂度表是"全网格遍历"的最坏情况。实际通过增量式处理,耗时大幅下降:

操作 最坏情况 增量式实测 降幅
各向异性凸起 2-3ms (20万顶点全遍历) 0.5-1ms(仅 BulgeOuterRadius 内,约 5-10% 顶点) -70%
PCA 边缘约束 3-5ms(全网格边缘) 0.5-1.5ms(仅 AffectedEdges,约 1-5% 边缘顶点) -70%
自适应细分 2-4ms(每次都做) 0.4-0.8ms(5 次锤击做一次,均摊) -80%
合计 7-12ms 1.4-3.3ms -75%

结论: 增量式处理后,design/01 自身开销约 1.4-3.3ms,留出 3.7-5.6ms 给 design/03 网格优化与其他子系统。

5.3.2 锤击事件 7ms 预算分配

按 §3.5 流水线分配预算:

步骤 子系统 预算 备注
1a-1c 双区形变 + 应变累积 design/01 1ms O(N) 增量
1d 边缘约束(可选) design/01 1ms 仅 AffectedEdges
1e 自适应细分(降频) design/01 0.5ms 5 次均摊
2 网格质量优化 design/03 1-2ms 边翻转/平滑/重网格
4 材料状态更新 design/04/06/07 0.5ms 公式应用
5a 法线重算 渲染层 0.5ms 全网格
5b 顶点颜色更新 design/02/05 0.3ms 全网格
6a BVH 重建 碰撞层 0.5-1ms 全量(增量改进见 §3.7.1)
6b 渲染同步 渲染层 0.1ms NotifyMeshUpdated
合计 5-7ms 在预算内

5.3.3 超预算时的降级策略

当实测超 7ms 时,按以下优先级降级(优先保证玩家可感知的体验):

优先级 降级项 影响
1(最先降) 关闭自适应细分(bEnableAdaptiveRefinement = false) 刀刃细节略差,形变仍正常
2 降低 design/03 重网格频率(RemeshCounter 周期 5 → 10) 网格质量略降,视觉无明显差异
3 关闭边缘约束(bEnableEdgeConstraint = false) 边缘略波纹,形变仍正常
4(最后降) 减小 ImpactRadius 影响玩家直觉,谨慎使用

禁止降级: 双区形变核心、法线重算、BVH 重建——这些是正确性保证。

5.3.4 帧率自适应联动

参考 requirements/03 §2 自动调参:

// 在 Tick 中检查帧率,动态调整优化强度
void ADynamicIronBlock::AutoAdjustOptimizationSettings()
{
    float CurrentFPS = 1.0f / GetWorld()->GetDeltaSeconds();

    if (CurrentFPS < 30.0f)
    {
        // 帧率低:降级
        bEnableAdaptiveRefinement = false;
        bEnableEdgeConstraint = false;
        RemeshCounterPeriod = 10;  // design/03 重网格频率
    }
    else if (CurrentFPS > 55.0f)
    {
        // 帧率高:恢复
        bEnableAdaptiveRefinement = true;
        bEnableEdgeConstraint = bUserEnabledEdgeConstraint;
        RemeshCounterPeriod = 5;
    }
}

5.3.5 与 design/03 的预算共享

design/01 与 design/03 共享"网格处理"预算(约 2.5-3.5ms 合计)。两者通过以下机制协调:

  • 共享受影响区域: design/01 输出 AffectedTriangles,design/03 直接使用,避免重复查找
  • 共享 EditMesh lock: 若选 §3.5.3 方案 A,两者在同一 EditMesh 内执行,避免多次解锁开销
  • 降频同步: 当 design/01 关闭自适应细分时,design/03 也应降低重网格频率

6. 调试工具

6.1 可视化流动方向

void ADynamicIronBlock::DebugDrawFlowDirections()
{
    FVector BoxHalfSize = GetBoxHalfSize();

    // 在网格表面采样点
    for (int32 VID : DynamicMesh->VertexIndicesItr())
    {
        if (VID % 100 != 0)  // 稀疏采样
            continue;

        FVector Pos = GetActorTransform().TransformPosition(DynamicMesh->GetVertex(VID));

        // 计算流动方向
        FVector ToNearestEdge = FindNearestFreeEdgeDirection(*DynamicMesh,
            DynamicMesh->GetVertex(VID), BoxHalfSize);
        FVector PrimaryFlow = CalculatePrimaryFlowDirection(FVector::DownVector, ToNearestEdge);

        // 绘制方向箭头
        DrawDebugDirectionalArrow(
            GetWorld(),
            Pos,
            Pos + GetActorTransform().TransformVector(PrimaryFlow) * 5.0f,
            2.0f,
            FColor::Cyan,
            false,
            5.0f,
            0,
            0.5f
        );
    }
}

6.2 可视化边缘顶点

void ADynamicIronBlock::DebugDrawEdgeVertices()
{
    TSet<int32> EdgeVertices;
    MarkEdgeVertices(*DynamicMesh, EdgeVertices, EdgeThreshold);

    for (int32 VID : EdgeVertices)
    {
        FVector Pos = GetActorTransform().TransformPosition(DynamicMesh->GetVertex(VID));
        DrawDebugPoint(GetWorld(), Pos, 8.0f, FColor::Red, false, 5.0f);
    }
}

7. 已知限制与未来改进

7.1 当前限制

  1. 极端形变: 当边缘厚度<1mm 时,各向异性假设可能失效
  2. 拓扑变化: 当前方法无法处理网格撕裂或融合(如锻打折叠)
  3. 多重边缘: 复杂形状(如叉状)的方向检测可能不准确
  4. 斜向锤击: §3.4 假设 HammerDir 通常为垂直(-Z),斜锤场景下 FindNearestFreeEdgeDirection 仍可用但 PrimaryFlow 的方向计算可能不符合玩家直觉。当前 P2 暂缓,见 §2.1 算法的 HammerDir 参数说明
  5. 体积守恒为可选项: §2.5 推荐策略 B 软守恒,但默认关闭(bEnableVolumeConservation = false),需要显式启用才能获得物理直觉正确的形变
  6. 受影响三角形集合未导出: §3.4 当前仅跟踪"边缘顶点",完整 AffectedTriangles 集合需扩展(参考 design/03 §8.2),才能完全支持 §3.5.3 方案 B 解耦调度

7.2 未来改进方向

  • 基于曲率张量的精确流动方向(替代当前 BoxHalfSize 启发式)
  • 支持拓扑变化(网格切割与连接)
  • 多工具系统(不同锤头形状)
  • 严格体积守恒(策略 C,拉格朗日乘子法)
  • 增量式 BVH 更新(全量重建 → 仅受影响区域)
  • 斜向锤击支持(HammerDir 一般化处理)

8. 引用