大家好,最近我挖了一个新的开源项目坑:N-Body 模拟,这是一个纯粹由兴趣驱动的项目,旨在通过编程模拟天体间的万有引力,并欣赏由物理规律所生成的优美图形。
在这个项目中,有一个核心环节是绘制天体的运行轨迹。轨迹本质上是一条由无数个点连接而成的曲线。为了高效存储这些点,我使用了一个 CircularBuffer,即环形缓冲区。它的内部实现相当经典:一个数组加上两个指针,分别标记数据的有效起止位置,非常适合存储这种定长的流式数据。
初遇瓶颈:当轨迹长到令人抓狂
最初,我选择使用 Direct2D 的 DrawLine 方法来逐段绘制轨迹。代码的逻辑非常直观,就是遍历轨迹点,然后两两相连画线:- for (int i = 0; i < _lastSnapshot.Stars.Length; ++i)
- {
- StarSnapshot star = _lastSnapshot.Stars[i];
- StarUIProps prop = _uiProps[i];
- // 遍历每两个相邻的点,并绘制一条线段
- prop.TrackingHistory.Enumerate2((Vector2 from, Vector2 to, int i) =>
- {
- // 根据点的位置计算一个渐变透明度
- float alpha = 1.0f * i / (prop.TrackingHistory.Count - 1);
- Color4 color = new Color4(prop.Color.R, prop.Color.G, prop.Color.B, alpha);
-
- // 调用DrawLine API
- ctx.DrawLine(from, to, XResource.GetColor(color), 0.02f);
- });
- }
复制代码 在轨迹点不多的时候,这套方案跑得非常欢快。然而,当用户希望看到更长、更华丽的轨迹时,问题就暴露了。当点的数量达到 10万 个级别时,界面开始出现肉眼可见的卡顿和掉帧。很显然,性能瓶颈出现了,优化迫在眉睫。
量化问题:用数据说话
为了精准定位问题,我进行了一次简单的性能测试。我使用 Stopwatch 来记录在轨迹点数达到10万个时,整个绘制过程的耗时。- protected override void OnDraw(ID2D1DeviceContext ctx)
- {
- // ... 其他绘制准备工作 ...
- Stopwatch sw = Stopwatch.StartNew();
- DrawCore(ctx); // 核心绘制逻辑
- // 当轨迹点达到10万时,打印耗时
- if (_uiProps[0].TrackingHistory.Count == 100000)
- {
- sw.Elapsed.TotalMilliseconds.Dump();
- }
-
- // ... 其他效果处理 ...
- }
复制代码 测试结果相当不乐观,连续几次的耗时输出如下:- 50.0262
- 51.7592
- 51.0839
- 50.7521
- 50.838
复制代码 平均耗时稳定在 50毫秒 左右!这是一个什么概念?为了保证流畅的用户体验(比如 60 FPS),每一帧的渲染时间必须控制在 16.67毫秒 以内。现在 50 毫秒的耗时,意味着帧率已经掉到了 20 FPS 以下,卡顿是必然的结果。
柳暗花明:一次调用胜过十万次
既然 DrawLine 的循环调用是瓶颈,那么优化的思路就应该是减少调用的次数。在和朋友讨论后,我决定尝试使用 ID2D1PathGeometry 来重构绘制逻辑。
ID2D1PathGeometry 允许我们先在内存中构建一个完整的几何路径,然后一次性地将其提交给 GPU 进行绘制。新的代码如下:- // 先绘制轨迹
- for (int i = 0; i < _lastSnapshot.Stars.Length; ++i)
- {
- StarSnapshot star = _lastSnapshot.Stars[i];
- StarUIProps prop = _uiProps[i];
- if (prop.TrackingHistory.Count < 2) continue;
- // 1. 创建一个路径几何对象
- using ID2D1PathGeometry1 path = XResource.Direct2DFactory.CreatePathGeometry();
-
- // 2. 打开路径并获取一个"画笔" (GeometrySink)
- using ID2D1GeometrySink sink = path.Open();
-
- // 3. 定义路径的起点
- sink.BeginFigure(prop.TrackingHistory.First!.Value, FigureBegin.Hollow);
-
- // 4. 将所有的点批量添加到路径中
- prop.TrackingHistory.Enumerate((pt, index) =>
- {
- if (index > 0) { sink.AddLine(pt); }
- });
-
- // 5. 结束并关闭路径定义
- sink.EndFigure(FigureEnd.Open);
- sink.Close();
-
- // 6. 一次性将整个路径绘制出来
- ctx.DrawGeometry(path, XResource.GetColor(prop.Color), 0.02f);
- }
复制代码 改完代码后,我怀着忐忑的心情再次运行性能测试,结果让我大吃一惊:- 6.8739
- 6.4511
- 6.436
- 6.0901
- 5.9227
复制代码 平均耗时骤降到了 6毫秒 左右!性能几乎提升了 10倍!
来源:豆瓜网用户自行投稿发布,如果侵权,请联系站长删除 |