docs: 添加几何侧技术设计(刀刃形成/表面磨损/网格质量)

三个对应几何与外观的技术设计。design/01 已包含评估增补(参数语义统一、双区实现回填、流水线协作、形变连续性、碰撞渲染同步、体积守恒策略、性能预算协调),修复了 InverseTransformPosition 法线转换 bug。

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

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
This commit is contained in:
2026-08-11 08:58:26 +08:00
parent 1514d53df5
commit a9543b5909
3 changed files with 2786 additions and 0 deletions
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,635 @@
# 表面磨损-顶点属性与无漂移采样设计
**项目**: BladeSmith
**文档类型**: 技术设计(专题)
**版本**: 2.0
**最后更新**: 2026-08-11
**状态**: 整合自原 `2026-01-22-表面磨损系统细节.md`
> 本文档描述表面磨损系统的顶点属性存储、无漂移纹理采样与材质实现。需求见 [requirements/02-表面磨损与外观需求.md](../requirements/02-表面磨损与外观需求.md)。总体架构见 [00-锻造系统总体技术架构.md](00-锻造系统总体技术架构.md)。
---
## 1. 技术背景
铁锈层厚度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 扩展顶点属性
```cpp
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 初始化逻辑
```cpp
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 打磨度更新
```cpp
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 氧化层更新(可选)
```cpp
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. 材质实现
### 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材质函数)
```hlsl
// 输入参数
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 问题示例
**错误做法(会漂移)**:
```hlsl
// 基于世界坐标采样噪声
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:初始位置采样**
```hlsl
// 用初始的、不变的位置采样噪声
float3 InitialLocalPos = ReconstructInitialPosition(TexCoord1, VertexColor.b);
float Rust = Noise(InitialLocalPos * 10.0);
// 优点:
// - 顶点移动后,InitialLocalPos不变
// - 铁锈图案"锁定"在材料上
// - 空间连续:相邻顶点采样相似的噪声值
```
**方案2:种子采样**
```hlsl
// 用每个顶点的唯一种子采样
float Seed = VertexColor.b; // 0-1范围
float Rust = PseudoRandom(Seed * 1000.0);
// 优点:
// - 完全无漂移
// - 实现简单
// 缺点:
// - 空间不连续:相邻顶点可能有跳变
// - 需要高网格密度掩盖
```
**推荐**:方案1(初始位置采样)
### 6.3 完整的无漂移采样函数
```hlsl
/**
* 从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 大马士革钢层
```cpp
/**
* 标记大马士革层
* 在初始化时分配"材料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 划痕系统
```hlsl
// 基于初始位置的各向异性噪声(模拟划痕)
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 水渍和氧化
```hlsl
// 淬火时的水渍痕迹
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 材质优化
```hlsl
// 早期退出优化
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 顶点属性压缩
如果内存紧张,可以压缩属性:
```cpp
// 打磨度: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 可视化打磨度
```cpp
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 材质调试模式
在材质中添加调试开关:
```hlsl
// 材质参数
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 当前限制
1. **顶点密度瓶颈**: 20-50万顶点的细节仍不及2048²纹理
2. **通道紧张**: 4个顶点颜色通道不够用,需要UV1补充
3. **无物理反射**: 打磨效果仅通过粗糙度模拟,无真实光线追踪
### 10.2 未来改进方向
1. **混合方案**: 顶点属性(大区域) + 纹理细节(微观)
2. **动态纹理**: 运行时更新高分辨率磨损纹理
3. **程序化细节**: 材质中用噪声补充细节
4. **多层材质**: 支持油污、涂层、水渍等多层效果
5. **法线贴图**: 从打磨度生成动态法线贴图
---
## 11. 引用
- 对应需求: [../requirements/02-表面磨损与外观需求.md](../requirements/02-表面磨损与外观需求.md)
- 总体架构: [00-锻造系统总体技术架构.md](00-锻造系统总体技术架构.md)
- 原始设计文档(归档): [../_archive/plans/2026-01-22-表面磨损系统细节.md](../_archive/plans/2026-01-22-表面磨损系统细节.md)
File diff suppressed because it is too large Load Diff