Files
BladeSmith/docs/impl/2026-01-23-hammer-deformation-enhancement.md
T
starmen1265 56cb314799 docs: 归档原 plans 目录,迁移实施清单到 impl/
原 docs/plans/ 下的 10 份混合文档(需求+设计混杂)归档到 docs/_archive/plans/ 作为决策考古用途。hammer-deformation-enhancement 实施清单迁移到 docs/impl/。后续工作产出统一进入 docs/requirements/ 与 docs/design/ 两个清晰分层。

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

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

38 KiB
Raw Blame History

锤击形变系统增强实施计划

For Claude: REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.

Goal: 实现各向异性材料流动、顶点属性系统(打磨度、初始位置)、表面磨损可视化和基础网格质量维护

Architecture: 混合体素-顶点架构,体素存储全局物理场(温度),顶点存储局部材料历史(应变、硬度、打磨度)

Tech Stack: Unreal Engine 5.6 C++, GeometryScripting API, DynamicMesh3, 自定义顶点属性层


Task 1: 添加顶点属性层基础结构

Files:

  • Modify: Source/BladeSmith/DynamicIronBlock.h:44-60

Step 1: 在头文件中添加顶点属性声明

在 ADynamicIronBlock 类的 private 部分添加:

// ========== 顶点属性层(阶段1) ==========

// 顶点属性层 - 使用FDynamicMesh3的扩展属性系统
TSharedPtr<TDynamicMeshVertexAttribute<FVector<float>>> InitialPositions;  // 初始位置(不变,用于无漂移纹理采样)
TSharedPtr<TDynamicMeshVertexAttribute<FVector<float>>> AccumulatedStrain;  // 累积塑性应变
TSharedPtr<TDynamicMeshVertexAttribute<FVector<float>>> HardnessCoefficient; // 硬度系数(影响形变阻力)
TSharedPtr<TDynamicMeshVertexAttribute<FVector<float>>> PolishAmount;        // 打磨度(0=生锈,1=光亮)

Step 2: 添加属性初始化函数声明

在头文件的 private 部分添加函数声明:

/**
 * 初始化顶点属性层
 */
void InitializeVertexAttributes();

/**
 * 释放顶点属性层
 */
void ReleaseVertexAttributes();

Step 3: 在 OnConstruction 中调用初始化

在 Source/BladeSmith/DynamicIronBlock.cpp:236 之后添加:

// 初始化顶点属性层
InitializeVertexAttributes();

Step 4: 在 EndPlay 中调用释放

在 Source/BladeSmith/DynamicIronBlock.cpp:119 之前添加:

// 释放顶点属性层
ReleaseVertexAttributes();

Step 5: 编译检查

Run: 在 Visual Studio 中编译项目 Expected: 编译成功,无错误 Commit: git add Source/BladeSmith/DynamicIronBlock.h Source/BladeSmith/DynamicIronBlock.cpp && git commit -m "feat: 添加顶点属性层基础结构"


Task 2: 实现顶点属性初始化

Files:

  • Modify: Source/BladeSmith/DynamicIronBlock.cpp:990

Step 1: 实现初始化函数

在文件末尾添加:

void ADynamicIronBlock::InitializeVertexAttributes()
{
    UDynamicMeshComponent* DynMeshComp = GetDynamicMeshComponent();
    if (!DynMeshComp)
    {
        UE_LOG(LogTemp, Error, TEXT("DynamicIronBlock: Failed to get DynamicMeshComponent for vertex attributes"));
        return;
    }

    UDynamicMesh* DynMesh = DynMeshComp->GetDynamicMesh();
    if (!DynMesh)
    {
        UE_LOG(LogTemp, Error, TEXT("DynamicIronBlock: Failed to get DynamicMesh for vertex attributes"));
        return;
    }

    // 读取网格以访问底层FDynamicMesh3
    DynMesh->ProcessMesh([&](const FDynamicMesh3& ReadMesh)
    {
        // 注意:FDynamicMesh3是const的,我们需要在EditMesh中创建属性
        // 这里仅获取顶点数量用于预分配
        int32 VertexCount = ReadMesh.VertexCount();
        UE_LOG(LogTemp, Log, TEXT("DynamicIronBlock: Initializing vertex attributes for %d vertices"), VertexCount);
    });

    // 使用EditMesh创建属性层
    DynMesh->EditMesh([&](FDynamicMesh3& EditMesh)
    {
        // 创建初始位置属性
        InitialPositions = MakeShared<TDynamicMeshVertexAttribute<FVector<float>>>(EditMesh);
        InitialPositions->Initialize("InitialPosition");

        // 创建累积应变属性
        AccumulatedStrain = MakeShared<TDynamicMeshVertexAttribute<FVector<float>>>(EditMesh);
        AccumulatedStrain->Initialize("AccumulatedStrain");

        // 创建硬度系数属性
        HardnessCoefficient = MakeShared<TDynamicMeshVertexAttribute<FVector<float>>>(EditMesh);
        HardnessCoefficient->Initialize("HardnessCoefficient");

        // 创建打磨度属性
        PolishAmount = MakeShared<TDynamicMeshVertexAttribute<FVector<float>>>(EditMesh);
        PolishAmount->Initialize("PolishAmount");

        // 初始化所有顶点的属性值
        for (int32 VertexID : EditMesh.VertexIndicesItr())
        {
            FVector3d VertexPos = EditMesh.GetVertex(VertexID);

            // 初始位置 = 当前位置
            InitialPositions->SetValue(VertexID, FVector3f(VertexPos.X, VertexPos.Y, VertexPos.Z));

            // 累积应变 = 0
            AccumulatedStrain->SetValue(VertexID, 0.0f);

            // 硬度系数 = 1.0 (未硬化)
            HardnessCoefficient->SetValue(VertexID, 1.0f);

            // 打磨度 = 0.2 (轻微生锈)
            PolishAmount->SetValue(VertexID, 0.2f);
        }
    }, EDynamicMeshChangeType::GeneralEdit);

    UE_LOG(LogTemp, Log, TEXT("DynamicIronBlock: Vertex attributes initialized successfully"));
}

Step 2: 实现释放函数

在文件末尾添加:

void ADynamicIronBlock::ReleaseVertexAttributes()
{
    InitialPositions.Reset();
    AccumulatedStrain.Reset();
    HardnessCoefficient.Reset();
    PolishAmount.Reset();

    UE_LOG(LogTemp, Log, TEXT("DynamicIronBlock: Vertex attributes released"));
}

Step 3: 编译并测试

Run: 在UE5编辑器中启动游戏 Expected: 在Output Log中看到 "Vertex attributes initialized successfully" Commit: git add Source/BladeSmith/DynamicIronBlock.cpp && git commit -m "feat: 实现顶点属性初始化"


Task 3: 添加各向异性流动参数

Files:

  • Modify: Source/BladeSmith/DynamicIronBlock.h:75-76

Step 1: 添加可编辑参数

在材质属性区域添加:

// 各向异性比(主方向/次方向流动强度比,1.0=均匀,>1.0=方向性)
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Iron Block|Material Properties", meta = (ClampMin = "1.0", ClampMax = "10.0"))
float AnisotropyRatio = 3.0f;

// 是否启用边缘约束
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Iron Block|Material Properties")
bool bEnableEdgeConstraint = false;

// 边缘阈值(cm)- 用于检测边缘顶点
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Iron Block|Material Properties", meta = (ClampMin = "0.1", ClampMax = "5.0"))
float EdgeThreshold = 0.5f;

Step 2: 编译检查

Run: 在Visual Studio中编译 Expected: 编译成功 Commit: git add Source/BladeSmith/DynamicIronBlock.h && git commit -m "feat: 添加各向异性流动参数"


Task 4: 实现方向检测辅助函数

Files:

  • Modify: Source/BladeSmith/DynamicIronBlock.h:331

Step 1: 添加函数声明

在头文件 private 部分添加:

/**
 * 查找最近的自由边界方向
 * 材料会优先朝向自由边界流动(阻力最小)
 */
FVector FindNearestFreeEdgeDirection(const FVector& LocalPos, const FVector& BoxHalfSize) const;

/**
 * 计算材料流动的主方向
 * 主方向 = 朝向边缘 + 垂直于锤击方向
 */
FVector CalculatePrimaryFlowDirection(const FVector& HammerDir, const FVector& ToNearestEdge) const;

/**
 * 计算次要流动方向(垂直于主方向和锤击方向)
 */
FVector CalculateSecondaryFlowDirection(const FVector& HammerDir, const FVector& PrimaryFlow) const;

Step 2: 实现方向检测函数

在 Source/BladeSmith/DynamicIronBlock.cpp:1297 之后添加:

FVector ADynamicIronBlock::FindNearestFreeEdgeDirection(const FVector& LocalPos, const FVector& BoxHalfSize) const
{
    // 计算到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];
}

FVector ADynamicIronBlock::CalculatePrimaryFlowDirection(const FVector& HammerDir, const FVector& ToNearestEdge) const
{
    // 移除锤击方向的分量(材料不能向锤头方向流动)
    FVector PrimaryFlow = ToNearestEdge - HammerDir * FVector::DotProduct(ToNearestEdge, HammerDir);
    return PrimaryFlow.GetSafeNormal();
}

FVector ADynamicIronBlock::CalculateSecondaryFlowDirection(const FVector& HammerDir, const FVector& PrimaryFlow) const
{
    return FVector::CrossProduct(HammerDir, PrimaryFlow).GetSafeNormal();
}

Step 3: 编译检查

Run: 在Visual Studio中编译 Expected: 编译成功 Commit: git add Source/BladeSmith/DynamicIronBlock.h Source/BladeSmith/DynamicIronBlock.cpp && git commit -m "feat: 实现方向检测辅助函数"


Task 5: 重构OnHammerImpact以支持各向异性形变

Files:

  • Modify: Source/BladeSmith/DynamicIronBlock.cpp:272-380

Step 1: 备份原函数

先将现有函数注释掉,保留作为参考

Step 2: 重写OnHammerImpact主流程

void ADynamicIronBlock::OnHammerImpact(FVector HitLocation, FVector HitNormal, float ImpactRadius, float ImpactDepth, float ImpactForce)
{
    // 获取 DynamicMeshComponent
    UDynamicMeshComponent* DynMeshComp = GetDynamicMeshComponent();
    if (!DynMeshComp)
    {
        UE_LOG(LogTemp, Error, TEXT("DynamicIronBlock: Failed to get DynamicMeshComponent for hammer impact"));
        return;
    }

    // 获取 DynamicMesh
    UDynamicMesh* DynMesh = DynMeshComp->GetDynamicMesh();
    if (!DynMesh)
    {
        UE_LOG(LogTemp, Error, TEXT("DynamicIronBlock: Failed to get DynamicMesh for hammer impact"));
        return;
    }

    // 限制力度范围在0-1之间
    ImpactForce = FMath::Clamp(ImpactForce, 0.0f, 1.0f);

    // 将世界坐标转换为局部坐标
    FTransform ActorTransform = GetActorTransform();
    FVector LocalHitLocation = ActorTransform.InverseTransformPosition(HitLocation);
    FVector LocalHitNormal = ActorTransform.InverseTransformVector(HitNormal).GetSafeNormal();

    // 计算实际的形变深度
    float ActualDepth = ImpactDepth * ImpactForce;

    UE_LOG(LogTemp, Log, TEXT("DynamicIronBlock: Hammer impact at local position (%f, %f, %f), depth: %f, radius: %f, anisotropy: %f"),
        LocalHitLocation.X, LocalHitLocation.Y, LocalHitLocation.Z, ActualDepth, ImpactRadius, AnisotropyRatio);

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

    // 使用GeometryScript修改网格顶点
    DynMesh->EditMesh([&](FDynamicMesh3& EditMesh)
    {
        // 遍历所有顶点
        for (int32 VertexID : EditMesh.VertexIndicesItr())
        {
            // 获取顶点位置
            FVector3d VertexPos = EditMesh.GetVertex(VertexID);
            FVector VertexPosFloat(VertexPos.X, VertexPos.Y, VertexPos.Z);

            // 计算顶点到锤击点的距离
            float Distance = FVector::Dist(VertexPosFloat, LocalHitLocation);

            // 只处理影响范围内的顶点
            float BulgeOuterRadius = ImpactRadius * (1.0f + BulgeRatio);
            if (Distance >= BulgeOuterRadius)
                continue;

            FVector Displacement = FVector::ZeroVector;

            // 压缩区:顶点在锤击半径内,向下压
            if (Distance < ImpactRadius)
            {
                // 计算衰减因子(使用幂函数实现平滑过渡)
                float NormalizedDist = Distance / ImpactRadius;  // 归一化距离 [0, 1]
                float Falloff = 1.0f - FMath::Pow(NormalizedDist, FalloffExponent);  // 衰减系数

                // 计算该顶点的形变偏移量
                float VertexDeformation = ActualDepth * Falloff;

                // 沿着锤击法线方向进行形变(向内压)
                Displacement = -LocalHitNormal * VertexDeformation;
            }
            // 凸起区:顶点在压缩区外、凸起区内,材料向上和向外流动
            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);

                    // 计算凸起高度(基于体积守恒和BulgeStrength)
                    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;
                }
            }

            // 应用形变
            if (!Displacement.IsNearlyZero())
            {
                FVector3d NewPosition = VertexPos + FVector3d(Displacement.X, Displacement.Y, Displacement.Z);
                EditMesh.SetVertex(VertexID, NewPosition);

                // 更新累积应变(累加位移量)
                if (AccumulatedStrain.IsValid())
                {
                    float CurrentStrain = AccumulatedStrain->GetValue(VertexID);
                    float StrainDelta = Displacement.Size();
                    AccumulatedStrain->SetValue(VertexID, CurrentStrain + StrainDelta);
                }
            }
        }
    }, EDynamicMeshChangeType::GeneralEdit, EDynamicMeshAttributeChangeFlags::Unknown, false);

    // 重新计算法线
    FGeometryScriptCalculateNormalsOptions NormalOp;
    UGeometryScriptLibrary_MeshNormalsFunctions::RecomputeNormals(DynMesh, NormalOp);

    // 通知组件网格已更新
    DynMeshComp->NotifyMeshUpdated();

    // 重新生成 BVH
    GenerateBVH();

    UE_LOG(LogTemp, Log, TEXT("DynamicIronBlock: Anisotropic hammer impact applied successfully"));
}

Step 3: 编译并测试

Run: 在UE5编辑器中启动游戏,执行锤击 Expected: 锤击后材料沿边缘方向流动更明显 Commit: git add Source/BladeSmith/DynamicIronBlock.cpp && git commit -m "feat: 实现各向异性材料流动形变"


Task 6: 实现边缘顶点检测和约束

Files:

  • Modify: Source/BladeSmith/DynamicIronBlock.h:331

Step 1: 添加函数声明

在头文件 private 部分添加:

/**
 * 检测并标记边缘顶点
 */
void MarkEdgeVertices(const FDynamicMesh3& Mesh, TSet<int32>& OutEdgeVertices) const;

/**
 * 对边缘顶点应用线性约束
 */
void ApplyEdgeLinearConstraint(FDynamicMesh3& Mesh, const TSet<int32>& EdgeVertices);

Step 2: 实现边缘检测

在 Source/BladeSmith/DynamicIronBlock.cpp:1450 之后添加:

void ADynamicIronBlock::MarkEdgeVertices(const FDynamicMesh3& Mesh, TSet<int32>& OutEdgeVertices) const
{
    FVector BoxHalfSize = BoxSize * 0.5f;
    float EdgeThresholdCM = EdgeThreshold;

    for (int32 VID : Mesh.VertexIndicesItr())
    {
        FVector3d Pos = Mesh.GetVertex(VID);
        FVector PosFloat(Pos.X, Pos.Y, Pos.Z);

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

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

void ADynamicIronBlock::ApplyEdgeLinearConstraint(FDynamicMesh3& Mesh, const TSet<int32>& EdgeVertices)
{
    // 简化版本:仅平滑边缘顶点,不进行完整的PCA拟合
    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)
        {
            // 计算邻居顶点的平均位置作为目标位置
            FVector3f AvgPos(0.0f, 0.0f, 0.0f);
            for (int32 NbrVID : EdgeNeighbors)
            {
                AvgPos += FVector3f(Mesh.GetVertex(NbrVID));
            }
            AvgPos /= EdgeNeighbors.Num();

            // 当前位置
            FVector3d CurrentPos = Mesh.GetVertex(VID);

            // 平滑更新(50%向平均位置移动)
            FVector3d SmoothPos = FMath::Lerp(CurrentPos, FVector3d(AvgPos.X, AvgPos.Y, AvgPos.Z), 0.5f);

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

Step 3: 在OnHammerImpact中集成边缘约束

在Task 5的 EditMesh 结束后、RecomputeNormals 之前添加:

// 边缘约束(如果启用)
if (bEnableEdgeConstraint)
{
    TSet<int32> EdgeVertices;
    MarkEdgeVertices(EditMesh, EdgeVertices);

    // 只处理受影响区域的边缘顶点
    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);
        UE_LOG(LogTemp, Log, TEXT("DynamicIronBlock: Applied edge constraint to %d vertices"), AffectedEdges.Num());
    }
}

Step 4: 编译并测试

Run: 在UE5编辑器中测试,启用 bEnableEdgeConstraint Expected: 边缘顶点保持更平整 Commit: git add Source/BladeSmith/DynamicIronBlock.h Source/BladeSmith/DynamicIronBlock.cpp && git commit -m "feat: 实现边缘顶点约束"


Task 7: 实现打磨度在锤击时的更新

Files:

  • Modify: Source/BladeSmith/DynamicIronBlock.cpp:420

Step 1: 在OnHammerImpact的EditMesh中更新打磨度

在Task 5中应用形变的位置后添加:

// 更新打磨度(锤击使表面变亮)
if (PolishAmount.IsValid())
{
    float CurrentPolish = PolishAmount->GetValue(VertexID);
    // 打磨度增加量与形变量成正比
    float PolishIncrease = Displacement.Size() * 0.5f;
    float NewPolish = FMath::Clamp(CurrentPolish + PolishIncrease, 0.0f, 1.0f);
    PolishAmount->SetValue(VertexID, NewPolish);
}

Step 2: 编译并测试

Run: 锤击后检查打磨度是否增加 Commit: git add Source/BladeSmith/DynamicIronBlock.cpp && git commit -m "feat: 实现锤击时的打磨度更新"


Task 8: 更新顶点颜色以编码打磨度

Files:

  • Modify: Source/BladeSmith/DynamicIronBlock.cpp:812

Step 1: 修改UpdateVertexColors以支持打磨度

找到现有的 UpdateVertexColors 函数,修改颜色计算部分:

void ADynamicIronBlock::UpdateVertexColors()
{
    if (!bEnableTemperatureSystem || TemperatureGrid.Num() == 0)
    {
        return;
    }

    // 获取 DynamicMeshComponent
    UDynamicMeshComponent* DynMeshComp = GetDynamicMeshComponent();
    if (!DynMeshComp)
    {
        UE_LOG(LogTemp, Warning, TEXT("DynamicIronBlock: Cannot update vertex colors, DynamicMeshComponent is null"));
        return;
    }

    // 获取 DynamicMesh
    UDynamicMesh* DynMesh = DynMeshComp->GetDynamicMesh();
    if (!DynMesh)
    {
        UE_LOG(LogTemp, Warning, TEXT("DynamicIronBlock: Cannot update vertex colors, DynamicMesh is null"));
        return;
    }

    // 准备颜色列表
    TArray<FLinearColor> VertexColors;
    int32 VertexCount = 0;

    // 读取网格以获取顶点信息并计算颜色
    DynMesh->ProcessMesh([&](const FDynamicMesh3& ReadMesh)
    {
        VertexCount = ReadMesh.MaxVertexID();

        // 预分配颜色数组大小
        VertexColors.Reserve(VertexCount);

        for (int32 VertexID = 0; VertexID < VertexCount; ++VertexID)
        {
            if (!ReadMesh.IsVertex(VertexID))
            {
                VertexColors.Add(FLinearColor::Black);
                continue;
            }

            // 获取顶点位置(局部坐标)
            FVector3d VertexPos = ReadMesh.GetVertex(VertexID);
            FVector VertexPosFloat(VertexPos.X, VertexPos.Y, VertexPos.Z);

            // 采样该位置的温度
            float Temperature = SampleTemperatureAtPosition(VertexPosFloat);

            // 将温度转换为黑体辐射颜色
            FLinearColor TempColor = TemperatureToColor(Temperature);

            // 获取打磨度并混合颜色
            float Polish = 0.2f;  // 默认值
            if (PolishAmount.IsValid())
            {
                Polish = PolishAmount->GetValue(VertexID);
            }

            // 生锈颜色(深红褐色)
            FLinearColor RustColor(0.4f, 0.2f, 0.15f);

            // 金属颜色(亮银色)
            FLinearColor MetalColor(0.9f, 0.9f, 0.95f);

            // 根据打磨度混合:0=生锈,1=光亮金属
            FLinearColor PolishColor = FMath::Lerp(RustColor, MetalColor, Polish);

            // 最终颜色 = 温度颜色 * 打磨颜色(乘法混合)
            FLinearColor FinalColor = TempColor * PolishColor;
            FinalColor.A = 1.0f;  // 不透明

            VertexColors.Add(FinalColor);
        }
    });

    // 创建 FGeometryScriptColorList 并关联颜色数组
    FGeometryScriptColorList ColorList;
    ColorList.List = MakeShared<TArray<FLinearColor>>(MoveTemp(VertexColors));

    // 使用 GeometryScript 设置顶点颜色
    UGeometryScriptLibrary_MeshVertexColorFunctions::SetMeshPerVertexColors(
        DynMesh,
        ColorList
    );

    // 通知组件网格已更新
    DynMeshComp->NotifyMeshUpdated();

    // 强制刷新渲染状态以确保顶点颜色更新
    DynMeshComp->MarkRenderStateDirty();
    DynMeshComp->MarkRenderDynamicDataDirty();

    UE_LOG(LogTemp, Log, TEXT("DynamicIronBlock: Vertex colors updated with polish information"));
}

Step 2: 编译并测试

Run: 锤击后观察表面颜色变化 Expected: 锤击区域从红褐色变为银色 Commit: git add Source/BladeSmith/DynamicIronBlock.cpp && git commit -m "feat: 更新顶点颜色以显示打磨度"


Task 9: 添加基础Laplacian平滑

Files:

  • Modify: Source/BladeSmith/DynamicIronBlock.h:331

Step 1: 添加函数声明

/**
 * 应用Laplacian平滑到受影响区域
 */
void ApplyLaplacianSmoothing(FDynamicMesh3& Mesh, const FVector& Center, float Radius, float Alpha = 0.3f);

Step 2: 实现平滑函数

在 Source/BladeSmith/DynamicIronBlock.cpp:1500 之后添加:

void ADynamicIronBlock::ApplyLaplacianSmoothing(FDynamicMesh3& Mesh, const FVector& Center, float Radius, float Alpha)
{
    // 创建位置副本用于迭代计算
    TMap<int32, FVector3d> NewPositions;

    for (int32 VertexID : Mesh.VertexIndicesItr())
    {
        FVector3d VertexPos = Mesh.GetVertex(VertexID);
        FVector PosFloat(VertexPos.X, VertexPos.Y, VertexPos.Z);

        // 只处理影响范围内的顶点
        if (FVector::Dist(PosFloat, Center) > Radius)
            continue;

        // 计算Laplacian(邻居顶点的平均位置)
        FVector3d Laplacian(0.0, 0.0, 0.0);
        int32 NeighborCount = 0;

        for (int32 NbrVID : Mesh.VtxVerticesItr(VertexID))
        {
            Laplacian += Mesh.GetVertex(NbrVID);
            NeighborCount++;
        }

        if (NeighborCount > 0)
        {
            Laplacian /= NeighborCount;

            // 平滑更新:新位置 = 原位置 + Alpha * (Laplacian - 原位置)
            FVector3d SmoothPos = FMath::Lerp(VertexPos, Laplacian, Alpha);
            NewPositions.Add(VertexID, SmoothPos);
        }
    }

    // 应用新位置
    for (const TPair<int32, FVector3d>& Pair : NewPositions)
    {
        Mesh.SetVertex(Pair.Key, Pair.Value);
    }
}

Step 3: 在OnHammerImpact中调用平滑

在Task 6的边缘约束之后添加:

// 应用Laplacian平滑(轻微,保持形状细节)
ApplyLaplacianSmoothing(EditMesh, LocalHitLocation, BulgeOuterRadius, 0.1f);

Step 4: 编译并测试

Run: 连续锤击50次,检查网格质量 Expected: 网格保持均匀,无严重退化 Commit: git add Source/BladeSmith/DynamicIronBlock.h Source/BladeSmith/DynamicIronBlock.cpp && git commit -m "feat: 添加Laplacian平滑以维护网格质量"


Task 10: 添加调试可视化工具

Files:

  • Modify: Source/BladeSmith/DynamicIronBlock.h:148

Step 1: 添加调试函数声明

/**
 * 调试函数 - 可视化材料流动方向
 */
UFUNCTION(BlueprintCallable, Category = "Hammer Impact|Debug")
void DebugDrawFlowDirections();

/**
 * 调试函数 - 可视化边缘顶点
 */
UFUNCTION(BlueprintCallable, Category = "Hammer Impact|Debug")
void DebugDrawEdgeVertices();

/**
 * 调试函数 - 输出顶点属性统计信息
 */
UFUNCTION(BlueprintCallable, Category = "Hammer Impact|Debug")
void DebugPrintVertexAttributes();

Step 2: 实现调试函数

在 Source/BladeSmith/DynamicIronBlock.cpp:1550 之后添加:

void ADynamicIronBlock::DebugDrawFlowDirections()
{
    if (!GetWorld())
        return;

    UDynamicMeshComponent* DynMeshComp = GetDynamicMeshComponent();
    if (!DynMeshComp)
        return;

    UDynamicMesh* DynMesh = DynMeshComp->GetDynamicMesh();
    if (!DynMesh)
        return;

    FVector BoxHalfSize = BoxSize * 0.5f;

    // 在网格表面采样点
    DynMesh->ProcessMesh([&](const FDynamicMesh3& ReadMesh)
    {
        int32 SampleCount = 0;
        for (int32 VertexID : ReadMesh.VertexIndicesItr())
        {
            // 每100个顶点采样一次
            if (SampleCount++ % 100 != 0)
                continue;

            FVector3d Pos3d = ReadMesh.GetVertex(VertexID);
            FVector Pos(Pos3d.X, Pos3d.Y, Pos3d.Z);

            // 计算流动方向
            FVector ToNearestEdge = FindNearestFreeEdgeDirection(Pos, BoxHalfSize);
            FVector PrimaryFlow = CalculatePrimaryFlowDirection(FVector::DownVector, ToNearestEdge);

            // 转换到世界坐标
            FVector WorldPos = GetActorTransform().TransformPosition(Pos);
            FVector WorldDir = GetActorTransform().TransformVector(PrimaryFlow);

            // 绘制方向箭头
            DrawDebugDirectionalArrow(
                GetWorld(),
                WorldPos,
                WorldPos + WorldDir * 5.0f,
                100.0f,  // 箭头大小
                FColor::Cyan,
                false,   // 是否持久
                5.0f,    // 持续时间
                0,       // 深度优先级
                0.5f     // 箭头厚度
            );
        }
    });

    UE_LOG(LogTemp, Warning, TEXT("DynamicIronBlock: Flow direction debug visualization drawn"));
}

void ADynamicIronBlock::DebugDrawEdgeVertices()
{
    if (!GetWorld())
        return;

    UDynamicMeshComponent* DynMeshComp = GetDynamicMeshComponent();
    if (!DynMeshComp)
        return;

    UDynamicMesh* DynMesh = DynMeshComp->GetDynamicMesh();
    if (!DynMesh)
        return;

    TSet<int32> EdgeVertices;
    DynMesh->ProcessMesh([&](const FDynamicMesh3& ReadMesh)
    {
        MarkEdgeVertices(ReadMesh, EdgeVertices);
    });

    for (int32 VID : EdgeVertices)
    {
        DynMesh->ProcessMesh([&](const FDynamicMesh3& ReadMesh)
        {
            if (!ReadMesh.IsVertex(VID))
                return;

            FVector3d Pos3d = ReadMesh.GetVertex(VID);
            FVector WorldPos = GetActorTransform().TransformPosition(FVector(Pos3d.X, Pos3d.Y, Pos3d.Z));

            DrawDebugPoint(GetWorld(), WorldPos, 8.0f, FColor::Red, false, 5.0f);
        });
    }

    UE_LOG(LogTemp, Warning, TEXT("DynamicIronBlock: Edge vertices debug visualization drawn (%d vertices)"), EdgeVertices.Num());
}

void ADynamicIronBlock::DebugPrintVertexAttributes()
{
    UDynamicMeshComponent* DynMeshComp = GetDynamicMeshComponent();
    if (!DynMeshComp)
        return;

    UDynamicMesh* DynMesh = DynMeshComp->GetDynamicMesh();
    if (!DynMesh)
        return;

    if (!AccumulatedStrain.IsValid() || !PolishAmount.IsValid())
    {
        UE_LOG(LogTemp, Warning, TEXT("DynamicIronBlock: Vertex attributes not initialized"));
        return;
    }

    // 统计信息
    float MinStrain = FLT_MAX, MaxStrain = -FLT_MAX, AvgStrain = 0.0f;
    float MinPolish = FLT_MAX, MaxPolish = -FLT_MAX, AvgPolish = 0.0f;
    int32 Count = 0;

    DynMesh->ProcessMesh([&](const FDynamicMesh3& ReadMesh)
    {
        for (int32 VertexID : ReadMesh.VertexIndicesItr())
        {
            float Strain = AccumulatedStrain->GetValue(VertexID);
            float Polish = PolishAmount->GetValue(VertexID);

            MinStrain = FMath::Min(MinStrain, Strain);
            MaxStrain = FMath::Max(MaxStrain, Strain);
            AvgStrain += Strain;

            MinPolish = FMath::Min(MinPolish, Polish);
            MaxPolish = FMath::Max(MaxPolish, Polish);
            AvgPolish += Polish;

            Count++;
        }
    });

    if (Count > 0)
    {
        AvgStrain /= Count;
        AvgPolish /= Count;
    }

    UE_LOG(LogTemp, Warning, TEXT("========== Vertex Attributes Debug =========="));
    UE_LOG(LogTemp, Warning, TEXT("Vertices: %d"), Count);
    UE_LOG(LogTemp, Warning, TEXT("Strain:    Min=%.3f, Max=%.3f, Avg=%.3f"), MinStrain, MaxStrain, AvgStrain);
    UE_LOG(LogTemp, Warning, TEXT("Polish:    Min=%.3f, Max=%.3f, Avg=%.3f"), MinPolish, MaxPolish, AvgPolish);
    UE_LOG(LogTemp, Warning, TEXT("==========================================="));

    // 屏幕显示
    if (GEngine)
    {
        GEngine->AddOnScreenDebugMessage(-1, 5.0f, FColor::Yellow,
            FString::Printf(TEXT("Strain: Avg=%.2f | Polish: Avg=%.2f"), AvgStrain, AvgPolish));
    }
}

Step 3: 编译并测试

Run: 在编辑器中调用调试函数 Expected: 看到流动方向箭头和边缘顶点标记 Commit: git add Source/BladeSmith/DynamicIronBlock.h Source/BladeSmith/DynamicIronBlock.cpp && git commit -m "feat: 添加调试可视化工具"


Task 11: 创建测试场景和验证

Files:

  • Create: Content/Tests/HammerDeformation_Test.umap

Step 1: 创建测试关卡

  1. 在UE5编辑器中创建新关卡 Content/Tests/HammerDeformation_Test.umap
  2. 添加 ADynamicIronBlock 到场景
  3. 配置不同的参数组合进行测试

Step 2: 测试用例1 - 锻造平面

AnisotropyRatio = 1.0
ImpactRadius = 5.0
BulgeStrength = 0.5
bEnableEdgeConstraint = false

操作: 在铁块顶部中心锤击10次 预期: 材料均匀扩散,表面平整

Step 3: 测试用例2 - 锻造边缘

AnisotropyRatio = 4.0
ImpactRadius = 3.0
BulgeStrength = 0.3
bEnableEdgeConstraint = true
EdgeThreshold = 1.0

操作: 在铁块边缘附近锤击20次 预期: 材料沿边缘方向流动,边缘保持平整

Step 4: 测试用例3 - 打磨度测试

操作: 锤击50次,调用 DebugPrintVertexAttributes 预期: 打磨度逐渐增加,表面颜色从红褐色变为银色

Step 5: 测试用例4 - 网格质量测试

操作: 连续快速锤击100次 预期: 网格保持均匀,无明显退化,60 FPS稳定

Step 6: 记录测试结果

创建文档 docs/tests/hammer-deformation-test-results.md 记录测试结果

Commit: git add Content/Tests/HammerDeformation_Test.umap docs/tests/hammer-deformation-test-results.md && git commit -m "test: 添加锤击形变测试场景和结果"


Task 12: 性能优化和参数调优

Files:

  • Modify: Source/BladeSmith/DynamicIronBlock.h:75-76

Step 1: 添加性能配置参数

// 是否启用自适应优化(仅在锤击间隔较长时执行完整优化)
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Iron Block|Performance")
bool bEnableAdaptiveOptimization = true;

// 最小平滑间隔(秒,低于此值跳过平滑)
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Iron Block|Performance", meta = (ClampMin = "0.0", ClampMax = "1.0"))
float MinSmoothingInterval = 0.1f;

Step 2: 添加时间追踪成员

在头文件 private 部分添加:

float LastHammerImpactTime = 0.0f;

Step 3: 在OnHammerImpact中应用自适应优化

修改Task 9的平滑调用为:

// 记录锤击时间
float CurrentTime = GetWorld()->GetTimeSeconds();

// 自适应优化:仅在锤击间隔足够大时执行完整平滑
bool bShouldSmooth = !bEnableAdaptiveOptimization || (CurrentTime - LastHammerImpactTime > MinSmoothingInterval);

if (bShouldSmooth)
{
    // 应用Laplacian平滑(轻微,保持形状细节)
    ApplyLaplacianSmoothing(EditMesh, LocalHitLocation, BulgeOuterRadius, 0.1f);
}

LastHammerImpactTime = CurrentTime;

Step 4: 使用Unreal Insights进行性能分析

Run: 启动游戏并启用Unreal Insights Expected: 锤击响应时间<10ms

Step 5: 根据分析结果调优参数

根据实际性能测试结果调整:

  • AnisotropyRatio 默认值
  • BulgeStrength 默认值
  • FalloffExponent 默认值
  • 平滑 Alpha 值

Commit: git add Source/BladeSmith/DynamicIronBlock.h Source/BladeSmith/DynamicIronBlock.cpp && git commit -m "perf: 添加自适应优化和性能调优"


Task 13: 文档更新

Files:

  • Modify: CLAUDE.md
  • Create: docs/features/hammer-deformation-system.md

Step 1: 更新CLAUDE.md

在架构概述部分添加:

### 核心系统(阶段1更新后)

**1. 动态网格形变系统(增强)**
- `ADynamicIronBlock` - 支持各向异性材料流动
- 顶点属性层:初始位置、累积应变、硬度系数、打磨度
- 边缘检测与约束:保持刀刃平整锐利
- 表面磨损可视化:基于打磨度的颜色混合

**关键特性:**
- 各向异性材料流动(AnisotropyRatio控制方向性)
- 锤击时自动更新打磨度(表面从生锈到光亮)
- Laplacian平滑维护网格质量
- 自适应优化策略

Step 2: 创建功能文档

创建 docs/features/hammer-deformation-system.md:

# 锤击形变系统

## 概述

实现高真实度的锻造锤击形变,支持各向异性材料流动、边缘约束、表面磨损和网格质量维护。

## 参数说明

### 材料属性

- **FalloffExponent** (2.0): 控制边缘过渡的平滑度
- **BulgeRatio** (0.8): 凸起区域半径相对于压缩区域的倍数
- **BulgeStrength** (0.5): 凸起高度强度
- **AnisotropyRatio** (3.0): 各向异性比,控制材料流动方向性
  - 1.0 = 均匀流动(锻造平面)
  - 3.0-5.0 = 方向性流动(锻造边缘)
  - 8.0-10.0 = 极端定向(精细整形)

### 边缘约束

- **bEnableEdgeConstraint** (false): 是否启用边缘平整约束
- **EdgeThreshold** (0.5cm): 边缘检测阈值

### 性能

- **bEnableAdaptiveOptimization** (true): 自适应优化开关
- **MinSmoothingInterval** (0.1s): 最小平滑间隔

## 使用方法

### 基础锤击

```cpp
IronBlock->OnHammerImpact(
    HitLocation,      // 锤击位置(世界坐标)
    HitNormal,        // 锤击法线
    5.0f,            // 影响半径(cm)
    2.0f,            // 形变深度(cm)
    1.0f             // 锤击力度(0-1)
);

调试工具

// 可视化材料流动方向
IronBlock->DebugDrawFlowDirections();

// 可视化边缘顶点
IronBlock->DebugDrawEdgeVertices();

// 输出顶点属性统计
IronBlock->DebugPrintVertexAttributes();

顶点属性系统

每个顶点存储以下属性:

  • InitialPositions: 初始位置,用于无漂移纹理采样
  • AccumulatedStrain: 累积塑性应变,记录形变历史
  • HardnessCoefficient: 硬度系数(预留,阶段3使用)
  • PolishAmount: 打磨度,0=生锈,1=光亮

性能指标

  • 目标帧时间: <7ms/次锤击
  • 支持顶点数: 20-50万
  • 稳定帧率: 60 FPS @ 连续锤击

验收标准

  • 各向异性材料流动正常工作
  • 锤击后表面颜色变化(铁锈→金属)
  • 边缘约束保持刀刃平整
  • 连续锤击100次网格质量良好
  • 性能达标,60 FPS稳定

**Step 3: 提交文档**

Commit: `git add CLAUDE.md docs/features/hammer-deformation-system.md && git commit -m "docs: 更新锤击形变系统文档"`

---

## 验收标准总结

完成所有任务后,系统应满足:

1. **功能完整性**
   - [ ] 各向异性材料流动工作正常
   - [ ] 顶点属性层正确初始化和更新
   - [ ] 打磨度在锤击时正确增加
   - [ ] 表面颜色从红褐色变为银色
   - [ ] 边缘约束保持刀刃平整
   - [ ] Laplacian平滑维护网格质量

2. **性能要求**
   - [ ] 单次锤击响应时间 <10ms
   - [ ] 连续锤击60 FPS稳定
   - [ ] 20万顶点网格性能良好

3. **可调试性**
   - [ ] 流动方向可视化工具
   - [ ] 边缘顶点可视化工具
   - [ ] 顶点属性统计输出
   - [ ] 所有参数可在编辑器调节

4. **代码质量**
   - [ ] 遵循项目编码规范
   - [ ] 每个Task独立提交
   - [ ] 提交信息清晰
   - [ ] 文档完整

---

**文档版本**: 1.0
**创建日期**: 2026-01-23
**预计工期**: 阶段1核心功能 (2-3天)
**状态**: 计划完成,待实施