三个对应几何与外观的技术设计。design/01 已包含评估增补(参数语义统一、双区实现回填、流水线协作、形变连续性、碰撞渲染同步、体积守恒策略、性能预算协调),修复了 InverseTransformPosition 法线转换 bug。 Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent) Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
42 KiB
刀刃形成-各向异性流动设计
项目: 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 优化建议
- 限制约束区域: 只对受影响的边缘顶点应用约束
- 降低细分频率: 每5-10次锤击才细分一次
- LOD策略: 远距离铁块使用简化约束
- 异步计算: 细分可以延后到帧尾执行
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 帧率自适应联动
// 在 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 当前限制
- 极端形变: 当边缘厚度<1mm 时,各向异性假设可能失效
- 拓扑变化: 当前方法无法处理网格撕裂或融合(如锻打折叠)
- 多重边缘: 复杂形状(如叉状)的方向检测可能不准确
- 斜向锤击: §3.4 假设 HammerDir 通常为垂直(-Z),斜锤场景下
FindNearestFreeEdgeDirection仍可用但PrimaryFlow的方向计算可能不符合玩家直觉。当前 P2 暂缓,见 §2.1 算法的HammerDir参数说明 - 体积守恒为可选项: §2.5 推荐策略 B 软守恒,但默认关闭(
bEnableVolumeConservation = false),需要显式启用才能获得物理直觉正确的形变 - 受影响三角形集合未导出: §3.4 当前仅跟踪"边缘顶点",完整
AffectedTriangles集合需扩展(参考 design/03 §8.2),才能完全支持 §3.5.3 方案 B 解耦调度
7.2 未来改进方向
- 基于曲率张量的精确流动方向(替代当前 BoxHalfSize 启发式)
- 支持拓扑变化(网格切割与连接)
- 多工具系统(不同锤头形状)
- 严格体积守恒(策略 C,拉格朗日乘子法)
- 增量式 BVH 更新(全量重建 → 仅受影响区域)
- 斜向锤击支持(HammerDir 一般化处理)
8. 引用
- 对应需求: ../requirements/01-锤击形变与刀刃形成需求.md
- 总体架构: 00-锻造系统总体技术架构.md
- 原始设计文档(归档): ../_archive/plans/2026-01-22-刀刃形成技术细节.md