Files
BladeSmith/docs/_archive/plans/2026-01-22-表面磨损系统细节.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

20 KiB
Raw Blame History

表面磨损系统细节

所属系统: 高真实度锻造系统 文档类型: 技术实现细节 最后更新: 2026-01-22


1. 需求概述

1.1 目标效果

  • 锤击后表面从"生锈"变"光亮",露出金属本体
  • 金属度(Metallic)增加:0.0 → 1.0
  • 粗糙度(Roughness)降低:0.9 → 0.2
  • 支持多种表面状态:铁锈、氧化层、酸蚀痕迹、划痕
  • 关键要求:表面特征必须随形变一起移动,不能"漂移"

1.2 铁锈的物理本质

真实铁锈层:

  • 厚度: 0.1-2mm(可视严重程度)
  • 成分: Fe₂O₃(红锈)、Fe₃O₄(黑锈)
  • 附着强度: 低,易被机械摩擦去除
  • 颜色: 橙红到深褐
  • 粗糙度: 高(微观凹凸不平)

锻造去锈原理:

  • 锤击压力 → 摩擦力 → 铁锈层破碎脱落
  • 塑性形变 → 新鲜金属表面暴露
  • 高温 → 部分氧化层还原

2. 技术方案:基于顶点属性 + 程序化细节

2.1 方案对比回顾

方案 分辨率 内存 性能 漂移问题 推荐度
基于顶点 ★★★☆☆ ★★★★★ ★★★★★ ✅无 ★★★★★
基于纹理 ★★★★★ ★★☆☆☆ ★★★☆☆ ⚠️有扭曲 ★★★☆☆
基于体素 ★★☆☆☆ ★★★☆☆ ★★★★☆ ✅无 ★★★☆☆

选择: 基于顶点属性(方案A)+ 程序化细节补充

2.2 数据通道分配

顶点颜色(RGBA)+ UV1通道的完整编码方案:

通道 用途 范围 更新频率 说明
R 温度可视化 0-1 每帧 从体素采样,黑体辐射颜色
G 打磨度(Polish) 0-1 锤击时 0=完全生锈,1=完全光亮
B 噪声种子(Seed) 0-1 初始化 用于程序化噪声,固定不变
A 硬度/应变 0-1 锤击时 可视化累积应变
UV1.x 初始位置X 0-1 初始化 编码3D位置的一部分
UV1.y 初始位置Y 0-1 初始化 编码3D位置的一部分

额外数据通过自定义顶点属性存储。


3. 数据结构定义

3.1 扩展顶点属性

class ADynamicIronBlock
{
private:
    // ===== 顶点自定义属性层 =====
    TDynamicMeshVertexAttribute<FVector>* InitialPositions;  // 初始局部位置
    TDynamicMeshVertexAttribute<float>* PolishAmount;        // 打磨度(0-1)
    TDynamicMeshVertexAttribute<float>* RustSeed;           // 铁锈噪声种子
    TDynamicMeshVertexAttribute<float>* OxideThickness;     // 氧化层厚度

    // ===== 材质参数 =====
    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Surface")
    float RustRoughness = 0.9f;         // 铁锈粗糙度

    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Surface")
    float MetalRoughness = 0.2f;        // 金属粗糙度

    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Surface")
    FLinearColor RustBaseColor = FLinearColor(0.6f, 0.3f, 0.1f);  // 铁锈基色

    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Surface")
    FLinearColor MetalBaseColor = FLinearColor(0.8f, 0.8f, 0.85f); // 金属基色

public:
    // 初始化顶点属性
    void InitializeVertexAttributes();
};

3.2 初始化逻辑

void ADynamicIronBlock::InitializeVertexAttributes()
{
    if (!DynamicMesh->HasMesh())
        return;

    DynamicMesh->EditMesh([&](FDynamicMesh3& Mesh)
    {
        // 确保属性系统已启用
        if (!Mesh.HasAttributes())
        {
            Mesh.EnableAttributes();
        }

        // 创建自定义属性层
        InitialPositions = new TDynamicMeshVertexAttribute<FVector>(&Mesh);
        InitialPositions->Initialize(FVector::ZeroVector);

        PolishAmount = new TDynamicMeshVertexAttribute<float>(&Mesh);
        PolishAmount->Initialize(0.0f);

        RustSeed = new TDynamicMeshVertexAttribute<float>(&Mesh);
        RustSeed->Initialize(0.0f);

        OxideThickness = new TDynamicMeshVertexAttribute<float>(&Mesh);
        OxideThickness->Initialize(1.0f);  // 初始完全氧化

        // 获取或创建UV1层
        FDynamicMeshUVOverlay* UV1 = Mesh.Attributes()->GetUVLayer(1);
        if (!UV1)
        {
            Mesh.Attributes()->SetNumUVLayers(2);
            UV1 = Mesh.Attributes()->GetUVLayer(1);
        }

        // 计算包围盒(用于位置编码)
        FAABB3 Box = Mesh.GetBounds();
        FVector BoxMin = Box.Min;
        FVector BoxMax = Box.Max;
        FVector BoxSize = Box.Max - Box.Min;

        // 初始化每个顶点
        for (int32 VID : Mesh.VertexIndicesItr())
        {
            FVector LocalPos = Mesh.GetVertex(VID);

            // 1. 保存初始位置(不变)
            InitialPositions->SetValue(VID, LocalPos);

            // 2. 初始化打磨度为0(完全生锈)
            PolishAmount->SetValue(VID, 0.0f);

            // 3. 分配随机铁锈种子
            RustSeed->SetValue(VID, FMath::FRand());

            // 4. 编码初始位置到UV1(仅X和Y)
            FVector2f EncodedPos;
            EncodedPos.X = (LocalPos.X - BoxMin.X) / (BoxSize.X + 1e-6f);
            EncodedPos.Y = (LocalPos.Y - BoxMin.Y) / (BoxSize.Y + 1e-6f);
            UV1->SetElement(VID, EncodedPos);

            // 5. 编码初始位置Z到顶点颜色.B通道(替换随机种子方案)
            FColor VertexColor = Mesh.GetVertexColor(VID);
            FLinearColor LinearColor(VertexColor);
            LinearColor.B = (LocalPos.Z - BoxMin.Z) / (BoxSize.Z + 1e-6f);  // Z坐标归一化
            LinearColor.G = 0.0f;  // 初始打磨度
            Mesh.SetVertexColor(VID, LinearColor.ToFColor());
        }
    });

    // 更新材质参数
    if (DynamicMaterialInstance)
    {
        DynamicMaterialInstance->SetScalarParameterValue(TEXT("RustRoughness"), RustRoughness);
        DynamicMaterialInstance->SetScalarParameterValue(TEXT("MetalRoughness"), MetalRoughness);
        DynamicMaterialInstance->SetVectorParameterValue(TEXT("RustColor"), RustBaseColor);
        DynamicMaterialInstance->SetVectorParameterValue(TEXT("MetalColor"), MetalBaseColor);
    }
}

4. 锤击更新逻辑

4.1 打磨度更新

void ADynamicIronBlock::OnHammerImpact(
    FVector HitLocation,
    FVector HitNormal,
    float ImpactRadius,
    float ImpactForce)
{
    FVector LocalHitPos = GetTransform().InverseTransformPosition(HitLocation);

    DynamicMesh->EditMesh([&](FDynamicMesh3& Mesh)
    {
        for (int32 VID : Mesh.VertexIndicesItr())
        {
            FVector VertexPos = Mesh.GetVertex(VID);
            float Distance = FVector::Dist(VertexPos, LocalHitPos);

            // 只更新受击区域
            if (Distance < ImpactRadius)
            {
                // 计算打磨强度
                float DistanceRatio = Distance / ImpactRadius;
                float FalloffExponent = 2.0f;
                float Intensity = FMath::Pow(1.0f - DistanceRatio, FalloffExponent);

                // 打磨强度与力度成正比
                float PolishIncrease = Intensity * ImpactForce * 0.001f;  // 可调系数

                // 累积打磨度(不可逆:只能增加,不能减少)
                float CurrentPolish = PolishAmount->GetValue(VID);
                float NewPolish = FMath::Min(1.0f, CurrentPolish + PolishIncrease);

                PolishAmount->SetValue(VID, NewPolish);

                // 同步到顶点颜色.G通道
                FColor VertexColor = Mesh.GetVertexColor(VID);
                FLinearColor LinearColor(VertexColor);
                LinearColor.G = NewPolish;
                Mesh.SetVertexColor(VID, LinearColor.ToFColor());

                // 铁锈种子(顶点颜色.B)保持不变
                // 初始位置Z(已在.B中)保持不变
            }
        }
    });
}

4.2 氧化层更新(可选)

void ADynamicIronBlock::UpdateOxideLayer(float DeltaTime)
{
    if (!DynamicMesh->HasMesh())
        return;

    // 简化的氧化模型:
    // - 高温(>300°C)时氧化加快
    // - 打磨区域氧化较慢(新鲜表面)

    DynamicMesh->EditMesh([&](FDynamicMesh3& Mesh)
    {
        for (int32 VID : Mesh.VertexIndicesItr())
        {
            float Temperature = SampleTemperatureAtVertex(Mesh, VID);
            float Polish = PolishAmount->GetValue(VID);

            // 氧化速率
            float BaseOxidationRate = 0.01f;  // 基础速率
            float TemperatureFactor = FMath::Clamp((Temperature - 300.0f) / 1000.0f, 0.0f, 1.0f);
            float PolishProtection = 1.0f - Polish * 0.8f;  // 打磨后氧化慢

            float OxidationRate = BaseOxidationRate * (1.0f + TemperatureFactor * 10.0f) * PolishProtection;

            // 更新氧化层厚度
            float CurrentOxide = OxideThickness->GetValue(VID);
            float NewOxide = FMath::Min(1.0f, CurrentOxide + OxidationRate * DeltaTime);
            OxideThickness->SetValue(VID, NewOxide);
        }
    });
}

5. 材质实现(UE5材质节点)

5.1 材质图表结构

[VertexColor] ────────┐
                     ├─> [Polish = VertexColor.g]
[TexCoord1] ─────────┘

[Polish] ──> [Lerp]
              ├─> [RustColor]  (Polish=0)
              └─> [MetalColor] (Polish=1)

[Polish] ──> [Lerp]
              ├─> [RustRoughness] (0.9)
              └─> [MetalRoughness] (0.2)

[Polish] ──> [Lerp]
              ├─> [RustMetallic] (0.0)
              └─> [MetalMetallic] (1.0)

[TextureCoordinate1] ──> [重建初始位置]
                          ──> [噪声采样]
                          ──> [铁锈图案]

5.2 HLSL实现

创建自定义材质函数 MF_SurfaceWear:

// 输入参数
float3 WorldPosition;           // GetWorldPosition()
float3 WorldNormal;            // GetWorldNormal()
float4 VertexColor;            // GetVertexColor()
float2 TexCoord1;              // GetTexCoord(1)

// 材质参数(从材质实例传入)
float3 RustColor;              // 铁锈颜色
float3 MetalColor;             // 金属颜色
float RustRoughness;           // 铁锈粗糙度
float MetalRoughness;          // 金属粗糙度
float RustMetallic;            // 铁锈金属度(通常0)
float MetalMetallic;           // 金属金属度(通常1)

// ===== 主函数 =====

// 1. 读取打磨度
float Polish = VertexColor.g;  // 来自G通道

// 2. 重建初始位置(用于无漂移噪声采样)
// 假设包围盒是 [-50, 50] cm
float3 BoxMin = float3(-50.0, -50.0, -50.0);
float3 BoxMax = float3(50.0, 50.0, 50.0);
float3 BoxSize = BoxMax - BoxMin;

float InitialPosX = TexCoord1.x * BoxSize.x + BoxMin.x;
float InitialPosY = TexCoord1.y * BoxSize.y + BoxMin.y;
float InitialPosZ = VertexColor.b * BoxSize.z + BoxMin.z;

float3 InitialLocalPos = float3(InitialPosX, InitialPosY, InitialPosZ);

// 3. 基于初始位置采样噪声(空间连续,无漂移)
float RustSeedNoise = frac(InitialLocalPos.x * 0.1 + InitialLocalPos.y * 0.1 + InitialLocalPos.z * 0.1);
float RustMacro = PseudoRandom(RustSeedNoise * 100.0);  // 宏观铁锈斑块
float RustDetail = Noise(InitialLocalPos * 20.0);       // 细节纹理
float RustMicro = Noise(InitialLocalPos * 100.0);       // 微观细节

// 组合多层噪声
float RustPattern = RustMacro * 0.6 + RustDetail * 0.3 + RustMicro * 0.1;

// 4. 计算最终颜色、金属度、粗糙度
float3 FinalRustColor = RustColor * (0.8 + RustPattern * 0.4);  // 铁锈颜色变化
float3 FinalBaseColor = lerp(FinalRustColor, MetalColor, Polish);

float FinalMetallic = lerp(RustMetallic, MetalMetallic, Polish);
float FinalRoughness = lerp(RustRoughness, MetalRoughness, Polish);

// 5. 可选:添加划痕细节(基于初始位置的高频噪声)
float ScratchNoise = Noise(InitialLocalPos * 200.0) * 0.1;
FinalRoughness += ScratchNoise * (1.0 - Polish);  // 划痕在未打磨区域更明显

// 6. 输出
return float4(FinalBaseColor, FinalRoughness, FinalMetallic, 1.0);

5.3 材质节点连接

材质输出:
  Base Color ──> FinalBaseColor
  Roughness ──> FinalRoughness
  Metallic ──> FinalMetallic
  Normal ───> [默认法线]
  Ambient Occlusion ──> [可选]

6. 无漂移保证机制

6.1 问题示例

错误做法(会漂移):

// 基于世界坐标采样噪声
float3 WorldPos = GetWorldPosition();
float Rust = Noise(WorldPos * 10.0);

// 问题:
// 锻打前: 顶点A在(100, 50, 0),Noise → 0.3(铁锈)
// 锻打后: 顶点A移动到(105, 52, 2),Noise → 0.8(变光亮?)
// 铁锈"滑走"了!

6.2 正确做法(无漂移)

方案1:初始位置采样

// 用初始的、不变的位置采样噪声
float3 InitialLocalPos = ReconstructInitialPosition(TexCoord1, VertexColor.b);
float Rust = Noise(InitialLocalPos * 10.0);

// 优点:
// - 顶点移动后,InitialLocalPos不变
// - 铁锈图案"锁定"在材料上
// - 空间连续:相邻顶点采样相似的噪声值

方案2:种子采样

// 用每个顶点的唯一种子采样
float Seed = VertexColor.b;  // 0-1范围
float Rust = PseudoRandom(Seed * 1000.0);

// 优点:
// - 完全无漂移
// - 实现简单

// 缺点:
// - 空间不连续:相邻顶点可能有跳变
// - 需要高网格密度掩盖

推荐:方案1(初始位置采样)

6.3 完整的无漂移采样函数

/**
 * 从UV1和顶点颜色重建初始位置
 */
float3 ReconstructInitialPosition(float2 TexCoord1, float EncodedZ)
{
    // 假设包围盒 [-50, 50] cm
    float3 BoxMin = float3(-50.0, -50.0, -50.0);
    float3 BoxSize = float3(100.0, 100.0, 100.0);

    float InitialX = TexCoord1.x * BoxSize.x + BoxMin.x;
    float InitialY = TexCoord1.y * BoxSize.y + BoxMin.y;
    float InitialZ = EncodedZ * BoxSize.z + BoxMin.z;

    return float3(InitialX, InitialY, InitialZ);
}

/**
 * 采样铁锈图案(无漂移版本)
 */
float3 SampleRustPattern(float2 TexCoord1, float EncodedZ, float3 WorldNormal)
{
    // 重建初始位置
    float3 InitialLocalPos = ReconstructInitialPosition(TexCoord1, EncodedZ);

    // 多层噪声
    float Rust1 = Noise(InitialLocalPos * 10.0 + float3(1.0, 2.0, 3.0));  // 低频
    float Rust2 = Noise(InitialLocalPos * 30.0 + float3(4.0, 5.0, 6.0));  // 中频
    float Rust3 = Noise(InitialLocalPos * 100.0 + float3(7.0, 8.0, 9.0)); // 高频

    // 组合
    float RustPattern = Rust1 * 0.5 + Rust2 * 0.3 + Rust3 * 0.2;

    // 基于法线的变化(侧面铁锈更多)
    float VerticalFactor = abs(WorldNormal.z);  // z=0表示侧面
    RustPattern *= (0.7 + VerticalFactor * 0.3);

    return RustPattern;
}

7. 高级特性

7.1 酸蚀痕迹(大马士革钢)

/**
 * 标记大马士革层
 * 在初始化时分配"材料ID"
 */
void InitializeDamascusLayers(FDynamicMesh3& Mesh)
{
    TDynamicMeshVertexAttribute<int32>* MaterialLayerID =
        new TDynamicMeshVertexAttribute<int32>(&Mesh);

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

        // 简化的层状分布:基于Z坐标
        int32 LayerID = (int32)FMath::Floor(Pos.Z / 2.0f) % 2;  // 每2cm一层

        MaterialLayerID->SetValue(VID, LayerID);
    }
}

// 材质中基于LayerID选择颜色
float3 GetDamascusColor(float3 InitialLocalPos, float LayerID)
{
    // 高碳钢层(深色)
    float3 HighCarbonColor = float3(0.15, 0.1, 0.08);

    // 低碳钢层(浅色)
    float3 LowCarbonColor = float3(0.75, 0.7, 0.65);

    // 混合过渡
    float Mix = SmoothLayerPattern(InitialLocalPos);
    return lerp(HighCarbonColor, LowCarbonColor, Mix);
}

7.2 划痕系统

// 基于初始位置的各向异性噪声(模拟划痕)
float3 InitialPos = ReconstructInitialPosition(TexCoord1, VertexColor.b);

// 方向性划痕(沿着锤击方向)
float ScratchDirection = InitialPos.x * 50.0;  // X方向划痕
float ScratchPattern = Noise(float3(ScratchDirection, InitialPos.y * 20.0, 0.0));

float3 ScratchColor = lerp(float3(1.0, 1.0, 1.0), BaseColor, ScratchPattern);
float ScratchRoughness = Roughness * (1.0 + ScratchPattern * 0.5);

// 仅在部分区域应用
float ScratchMask = SmoothNoise(InitialPos * 15.0);
FinalBaseColor = lerp(FinalBaseColor, ScratchColor, ScratchMask * 0.3);

7.3 水渍和氧化

// 淬火时的水渍痕迹
float WaterStainMask = SampleWaterStainTexture(WorldPos);
float3 PatinaColor = float3(0.3, 0.4, 0.5);  // 蓝绿色氧化

// 仅在潮湿区域显示
float HumidityFactor = 0.3;  // 可调
FinalBaseColor = lerp(FinalBaseColor, FinalBaseColor * PatinaColor,
                      WaterStainMask * HumidityFactor);

8. 性能优化

8.1 材质优化

// 早期退出优化
float Polish = VertexColor.g;
if (Polish > 0.95)
{
    // 完全打磨:直接返回金属色,跳过噪声计算
    return MetalColor;
}

// LOD:基于距离减少噪声层数
float DistanceToCamera = length(View.WorldPosition);
int32 NoiseOctaves = DistanceToCamera < 100.0 ? 3 : 1;

8.2 顶点属性压缩

如果内存紧张,可以压缩属性:

// 打磨度:8位精度足够
uint8 CompressedPolish = FMath::RoundToInt(PolishAmount * 255.0f);
PolishAmount->SetValue(VID, CompressedPolish / 255.0f);

// 初始位置:16位量化
FVector QuantizedPos(
    FMath::RoundToInt(LocalPos.X * 100.0f) / 100.0f,
    FMath::RoundToInt(LocalPos.Y * 100.0f) / 100.0f,
    FMath::RoundToInt(LocalPos.Z * 100.0f) / 100.0f
);

9. 调试与可视化

9.1 可视化打磨度

void ADynamicIronBlock::DebugDrawPolishHeatmap()
{
    if (!DynamicMesh->HasMesh())
        return;

    for (int32 VID : DynamicMesh->VertexIndicesItr())
    {
        if (VID % 50 != 0)  // 稀疏采样
            continue;

        float Polish = PolishAmount->GetValue(VID);
        FVector WorldPos = GetActorTransform().TransformPosition(DynamicMesh->GetVertex(VID));

        // 颜色映射:蓝色(0)→ 绿色(0.5)→ 红色(1.0)
        FColor DebugColor = FMath::Lerp(
            FColor::Blue,
            FColor::Red,
            Polish
        );

        DrawDebugPoint(GetWorld(), WorldPos, 5.0f, DebugColor, false, 5.0f);
    }
}

9.2 材质调试模式

在材质中添加调试开关:

// 材质参数
int32 DebugMode;  // 0=正常, 1=显示打磨度, 2=显示铁锈图案, 3=显示初始位置

float3 DebugOutput;
switch (DebugMode)
{
    case 1:  // 打磨度热图
        DebugOutput = float3(Polish, 0.0, 1.0 - Polish);
        break;
    case 2:  // 铁锈图案
        DebugOutput = RustPattern;
        break;
    case 3:  // 初始位置可视化
        DebugOutput = frac(InitialLocalPos * 0.1);
        break;
    default:
        DebugOutput = FinalBaseColor;
        break;
}

return DebugOutput;

10. 参数配置示例

10.1 新铁块(完全生锈)

RoughRoughness = 0.95f;
MetalRoughness = 0.15f;
RustColor = FLinearColor(0.7f, 0.35f, 0.15f);  // 橙红色
MetalColor = FLinearColor(0.75f, 0.75f, 0.8f); // 钢铁色

10.2 半旧铁块(部分打磨)

RoughRoughness = 0.85f;
MetalRoughness = 0.25f;
RustColor = FLinearColor(0.5f, 0.25f, 0.1f);   // 深褐色
MetalColor = FLinearColor(0.8f, 0.8f, 0.85f);

10.3 高碳钢(大马士革)

RoughRoughness = 0.9f;
MetalRoughness = 0.2f;
RustColor = FLinearColor(0.3f, 0.15f, 0.1f);   // 深色层
MetalColor = FLinearColor(0.6f, 0.55f, 0.5f);  // 浅色层
bEnableDamascusLayers = true;

11. 已知限制与未来改进

11.1 当前限制

  1. 顶点密度瓶颈: 20-50万顶点的细节仍不及2048²纹理
  2. 通道紧张: 4个顶点颜色通道不够用,需要UV1补充
  3. 无物理反射: 打磨效果仅通过粗糙度模拟,无真实光线追踪

11.2 未来改进方向

  1. 混合方案: 顶点属性(大区域)+ 纹理细节(微观)
  2. 动态纹理: 运行时更新高分辨率磨损纹理
  3. 程序化细节: 材质中用噪声补充细节
  4. 多层材质: 支持油污、涂层、水渍等多层效果
  5. 法线贴图: 从打磨度生成动态法线贴图