20 KiB
20 KiB
表面磨损系统细节
所属系统: 高真实度锻造系统 文档类型: 技术实现细节 最后更新: 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 当前限制
- 顶点密度瓶颈: 20-50万顶点的细节仍不及2048²纹理
- 通道紧张: 4个顶点颜色通道不够用,需要UV1补充
- 无物理反射: 打磨效果仅通过粗糙度模拟,无真实光线追踪
11.2 未来改进方向
- 混合方案: 顶点属性(大区域)+ 纹理细节(微观)
- 动态纹理: 运行时更新高分辨率磨损纹理
- 程序化细节: 材质中用噪声补充细节
- 多层材质: 支持油污、涂层、水渍等多层效果
- 法线贴图: 从打磨度生成动态法线贴图