0%

可编辑曲面性能优化

之前的可编辑模型(如正方体,圆台)不涉及复杂的性能瓶颈,但是可编辑曲面不同。它的本质是沿着样条线(Spline)动态生成的拓扑结构,其顶点数量没有硬性上限,而是与路径长度采样精度以及断面复杂度呈线性甚至几何级数增长。

优化策略

埋点分析与耗时监控

“没有测量就没有优化。” 在盲目优化之前,必须知道瓶颈在哪。实现一个自定义的计时器类(Scoped Timer),在几何生成、碰撞构建等关键函数的入口进行埋点。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
struct FScopedTimer
{
FString FunctionName;
double StartTime;

static int32 Depth;

FScopedTimer(FString InName)
: FunctionName(InName)
{
StartTime = FPlatformTime::Seconds();
Depth++;
}

~FScopedTimer()
{
double EndTime = FPlatformTime::Seconds();
float DurationMs = (float)((EndTime - StartTime) * 1000.0);

FString Indent = TEXT("");
for (int32 i = 1; i < Depth; ++i)
{
Indent += TEXT(" | ");
}

FString LogLine = FString::Printf(TEXT("[%s] %s%s: %.4f ms\n"),
*FDateTime::Now().ToString(TEXT("%H:%M:%S.%s")),
*Indent,
*FunctionName,
DurationMs);

FString FilePath = FPaths::ProjectSavedDir() / TEXT("Profiling/SurfaceGenPerf.txt");
FFileHelper::SaveStringToFile(LogLine, *FilePath, FFileHelper::EEncodingOptions::AutoDetect, &IFileManager::Get(), FILEWRITE_Append);

Depth--;
}
};

int32 FScopedTimer::Depth = 0;

内存管理:缓存复用与预分配

C++ 中频繁的内存分配是非常昂贵的

  • 成员变量缓存:LeftRailRaw, CachedRawPoints 等高频使用的数组提升为类的成员变量。每次生成时,调用 Reset() 而不是 Empty()Reset() 会清空数据但保留 Capacity(容量),从而避免了底层的内存释放与重新分配。
  • 预分配 : 在填充数据前,通过 CalculateVertexCountEstimate() 估算大概的顶点数,并调用 MeshData.Reserve()。这避免了 TArrayAdd 过程中因为扩容而发生的多次数据搬迁。

智能减面:基于角度的自适应采样

并非所有的路段都需要同等密度的顶点采样。直线段仅需首尾两点确定,而弯道才需要高密度逼近。

  • 原理: 计算相邻线段方向向量的点积:
    $$\vec{v_1} \cdot \vec{v_2} = \cos(\theta)$$
    如果点积接近 1(例如 > 0.9998),说明线段几乎共线,中间的顶点是冗余的,可以剔除。
  • 效果: 在不损失视觉质量的前提下,大幅减少了生成的三角形数量,进而减轻了 GPU 的光栅化压力。

算法优化:空间哈希加速去环检测

当曲面发生自我交叉(打结)时,我们需要检测并剔除这些几何体。朴素的去环检测需要 $O(N^2)$ 的暴力比对,顶点过多容易卡死线程。

  • 解决方案: 引入空间哈希 (Spatial Hashing)

  • 实现: 我们将 3D 空间投影到 2D 网格,通过简单的位运算和大质数散射 ((X * 73856093) ^ (Y * 19349663)) & (HashSize - 1) 将顶点映射到哈希桶中。之后会执行精确的 3D 欧氏距离检测。只要高度(Z轴)差距足够大,即便 2D 投影重叠,也绝不会被错误剔除。

  • 提升: 查询复杂度降至接近 $O(1)$,实现实时拖拽去环。